Web InnoventixFreeCode

Gradient Fade Marquee

Original · free

marquee with edge fade

byWeb InnoventixReact + Tailwind
mqxgradientfademarquees
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/mqx-gradient-fade.json
mqx-gradient-fade.tsx
"use client";

import { useState, type CSSProperties, type ReactNode, type SVGProps } from "react";
import { useReducedMotion } from "motion/react";

type IconName =
  | "bolt"
  | "gauge"
  | "code"
  | "refresh"
  | "eye"
  | "layers"
  | "shield"
  | "globe"
  | "search"
  | "lock"
  | "sparkles"
  | "cog";

interface Capability {
  icon: IconName;
  title: string;
  desc: string;
}

interface Tag {
  icon: IconName;
  label: string;
}

const CAPABILITIES: Capability[] = [
  { icon: "bolt", title: "Edge Rendering", desc: "Streaming SSR from 310 cities" },
  { icon: "gauge", title: "Core Web Vitals", desc: "LCP under 1.2s on real 4G" },
  { icon: "code", title: "Type-Safe APIs", desc: "Database row to button click" },
  { icon: "refresh", title: "Zero-Downtime Deploys", desc: "Atomic release, instant rollback" },
  { icon: "eye", title: "Accessible by Default", desc: "WCAG 2.2 AA every component" },
  { icon: "layers", title: "Design Tokens", desc: "One source of truth, both themes" },
];

const TAGS: Tag[] = [
  { icon: "shield", label: "SOC 2 Type II" },
  { icon: "globe", label: "12 locales, RTL-ready" },
  { icon: "search", label: "Structured-data SEO" },
  { icon: "lock", label: "Passkey authentication" },
  { icon: "sparkles", label: "Motion that respects users" },
  { icon: "cog", label: "CI/CD on every push" },
  { icon: "gauge", label: "99.98% uptime SLA" },
  { icon: "code", label: "Open-source components" },
];

const FADE =
  "linear-gradient(to right, transparent 0, #000 9%, #000 91%, transparent 100%)";

const MASK_STYLE: CSSProperties = {
  WebkitMaskImage: FADE,
  maskImage: FADE,
};

function Icon({ name, className }: { name: IconName; className?: string }) {
  const common: SVGProps<SVGSVGElement> = {
    className,
    viewBox: "0 0 24 24",
    fill: "none",
    stroke: "currentColor",
    strokeWidth: 1.75,
    strokeLinecap: "round",
    strokeLinejoin: "round",
    "aria-hidden": true,
  };
  switch (name) {
    case "bolt":
      return (
        <svg {...common}>
          <path d="M13 2 5 13h5l-1 9 8-12h-5l1-8Z" />
        </svg>
      );
    case "gauge":
      return (
        <svg {...common}>
          <path d="M5 18a8 8 0 1 1 14 0" />
          <path d="M12 18l4-5" />
          <circle cx="12" cy="18" r="1.2" fill="currentColor" stroke="none" />
        </svg>
      );
    case "code":
      return (
        <svg {...common}>
          <path d="m9 8-4 4 4 4" />
          <path d="m15 8 4 4-4 4" />
        </svg>
      );
    case "refresh":
      return (
        <svg {...common}>
          <path d="M20 12a8 8 0 1 1-2.3-5.6" />
          <path d="M20 4v4h-4" />
        </svg>
      );
    case "eye":
      return (
        <svg {...common}>
          <path d="M2.5 12S6 5.5 12 5.5 21.5 12 21.5 12 18 18.5 12 18.5 2.5 12 2.5 12Z" />
          <circle cx="12" cy="12" r="3" />
        </svg>
      );
    case "layers":
      return (
        <svg {...common}>
          <path d="m12 3 9 5-9 5-9-5 9-5Z" />
          <path d="m3 13 9 5 9-5" />
        </svg>
      );
    case "shield":
      return (
        <svg {...common}>
          <path d="M12 3l7 3v5c0 4.4-3 7.6-7 9-4-1.4-7-4.6-7-9V6l7-3Z" />
          <path d="m9 12 2 2 4-4" />
        </svg>
      );
    case "globe":
      return (
        <svg {...common}>
          <circle cx="12" cy="12" r="9" />
          <path d="M3 12h18" />
          <path d="M12 3c2.7 2.4 4 5.6 4 9s-1.3 6.6-4 9c-2.7-2.4-4-5.6-4-9s1.3-6.6 4-9Z" />
        </svg>
      );
    case "search":
      return (
        <svg {...common}>
          <circle cx="11" cy="11" r="7" />
          <path d="m20 20-3.6-3.6" />
        </svg>
      );
    case "lock":
      return (
        <svg {...common}>
          <rect x="4.5" y="10" width="15" height="10.5" rx="2" />
          <path d="M8 10V7a4 4 0 0 1 8 0v3" />
        </svg>
      );
    case "sparkles":
      return (
        <svg {...common}>
          <path d="M12 3l1.7 4.6L18.5 9.4l-4.8 1.8L12 16l-1.7-4.8L5.5 9.4l4.8-1.8L12 3Z" />
          <path d="M18.6 15.4l.6 1.8 1.8.6-1.8.6-.6 1.8-.6-1.8-1.8-.6 1.8-.6.6-1.8Z" />
        </svg>
      );
    case "cog":
      return (
        <svg {...common}>
          <circle cx="12" cy="12" r="3.2" />
          <path d="M12 2.5v2.5M12 19v2.5M4.2 4.2l1.8 1.8M18 18l1.8 1.8M2.5 12H5M19 12h2.5M4.2 19.8 6 18M18 6l1.8-1.8" />
        </svg>
      );
    default:
      return null;
  }
}

function CapabilityCard({ item }: { item: Capability }) {
  return (
    <li className="mr-4 flex w-72 shrink-0 items-center gap-3 rounded-2xl border border-slate-200 bg-white/90 px-4 py-3 shadow-sm backdrop-blur dark:border-slate-800 dark:bg-slate-900/80">
      <span className="grid h-10 w-10 shrink-0 place-items-center rounded-xl bg-gradient-to-br from-indigo-500 to-violet-600 text-white shadow-sm">
        <Icon name={item.icon} className="h-5 w-5" />
      </span>
      <div className="min-w-0">
        <p className="truncate text-sm font-semibold text-slate-900 dark:text-slate-100">
          {item.title}
        </p>
        <p className="truncate text-xs text-slate-500 dark:text-slate-400">{item.desc}</p>
      </div>
    </li>
  );
}

function TagPill({ item }: { item: Tag }) {
  return (
    <li className="mr-3 flex shrink-0 items-center gap-2 rounded-full border border-slate-200 bg-slate-50 px-4 py-2 text-sm font-medium text-slate-700 dark:border-slate-800 dark:bg-slate-800/70 dark:text-slate-200">
      <Icon name={item.icon} className="h-4 w-4 text-indigo-500 dark:text-indigo-400" />
      <span className="whitespace-nowrap">{item.label}</span>
    </li>
  );
}

function Row({
  ariaLabel,
  content,
  trackStyle,
  reduced,
}: {
  ariaLabel: string;
  content: ReactNode;
  trackStyle: CSSProperties;
  reduced: boolean;
}) {
  return (
    <div
      role="group"
      aria-label={ariaLabel}
      tabIndex={reduced ? 0 : -1}
      className={`relative rounded-2xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 ${
        reduced ? "overflow-x-auto overflow-y-hidden" : "overflow-hidden"
      }`}
      style={MASK_STYLE}
    >
      <div className="mqxGF-track flex w-max" style={trackStyle}>
        <ul className="flex w-max shrink-0 items-stretch py-1">{content}</ul>
        {!reduced && (
          <ul className="flex w-max shrink-0 items-stretch py-1" aria-hidden="true">
            {content}
          </ul>
        )}
      </div>
    </div>
  );
}

export default function MqxGradientFade() {
  const prefersReduced = useReducedMotion() ?? false;
  const [playing, setPlaying] = useState(true);
  const [reversed, setReversed] = useState(false);
  const [speed, setSpeed] = useState(1);
  const [hovered, setHovered] = useState(false);

  const running = playing && !hovered && !prefersReduced;
  const dirA: "normal" | "reverse" = reversed ? "reverse" : "normal";
  const dirB: "normal" | "reverse" = reversed ? "normal" : "reverse";

  const trackStyle = (base: number, dir: "normal" | "reverse"): CSSProperties =>
    prefersReduced
      ? {}
      : {
          animationName: "mqxGF-scroll",
          animationDuration: `${(base / speed).toFixed(2)}s`,
          animationTimingFunction: "linear",
          animationIterationCount: "infinite",
          animationDirection: dir,
          animationPlayState: running ? "running" : "paused",
          willChange: "transform",
        };

  const buttonBase =
    "inline-flex items-center gap-2 rounded-full border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 shadow-sm transition hover:bg-slate-50 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200 dark:hover:bg-slate-700 dark:focus-visible:ring-offset-slate-950";

  const capabilityItems = CAPABILITIES.map((c) => (
    <CapabilityCard key={c.title} item={c} />
  ));
  const tagItems = TAGS.map((t) => <TagPill key={t.label} item={t} />);

  return (
    <section className="relative w-full overflow-hidden bg-gradient-to-b from-slate-50 to-white py-20 sm:py-28 dark:from-slate-950 dark:to-slate-900">
      <style>{`
        @keyframes mqxGF-scroll {
          from { transform: translate3d(0, 0, 0); }
          to { transform: translate3d(-50%, 0, 0); }
        }
        @media (prefers-reduced-motion: reduce) {
          .mqxGF-track { animation: none !important; transform: none !important; }
        }
      `}</style>

      <div
        aria-hidden="true"
        className="pointer-events-none absolute inset-x-0 top-0 h-72 bg-gradient-to-b from-indigo-100/60 to-transparent dark:from-indigo-500/10"
      />

      <div className="relative z-10 mx-auto max-w-6xl px-6">
        <div className="mx-auto max-w-2xl text-center">
          <span className="inline-flex items-center gap-2 rounded-full border border-indigo-200 bg-indigo-50 px-3 py-1 text-xs font-medium text-indigo-700 dark:border-indigo-500/30 dark:bg-indigo-500/10 dark:text-indigo-300">
            <span className="h-1.5 w-1.5 rounded-full bg-indigo-500" />
            Capabilities marquee
          </span>
          <h2 className="mt-5 text-balance text-3xl font-semibold tracking-tight text-slate-900 sm:text-4xl dark:text-slate-50">
            Everything ships production-ready
          </h2>
          <p className="mt-4 text-pretty text-base leading-relaxed text-slate-600 dark:text-slate-300">
            A running snapshot of what is baked into every build. Pause it, flip the
            direction, or slow it down to read each card. The edges fade so the motion
            reads as one continuous ribbon.
          </p>
        </div>

        <div className="mx-auto mt-10 flex max-w-3xl flex-wrap items-center justify-center gap-3">
          <button
            type="button"
            onClick={() => setPlaying((v) => !v)}
            aria-pressed={playing}
            aria-label={playing ? "Pause marquee" : "Play marquee"}
            disabled={prefersReduced}
            className={buttonBase}
          >
            {playing ? (
              <svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
                <path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z" />
              </svg>
            ) : (
              <svg viewBox="0 0 24 24" className="h-4 w-4" fill="currentColor" aria-hidden="true">
                <path d="M7 5v14l11-7L7 5Z" />
              </svg>
            )}
            {playing ? "Pause" : "Play"}
          </button>

          <button
            type="button"
            onClick={() => setReversed((v) => !v)}
            aria-pressed={reversed}
            disabled={prefersReduced}
            className={buttonBase}
          >
            <svg
              viewBox="0 0 24 24"
              className="h-4 w-4"
              fill="none"
              stroke="currentColor"
              strokeWidth={1.75}
              strokeLinecap="round"
              strokeLinejoin="round"
              aria-hidden="true"
            >
              <path d="M4 9h13l-3-3M20 15H7l3 3" />
            </svg>
            Reverse
          </button>

          <div className="inline-flex items-center gap-3 rounded-full border border-slate-300 bg-white px-4 py-2 text-sm text-slate-700 shadow-sm dark:border-slate-700 dark:bg-slate-800 dark:text-slate-200">
            <label htmlFor="mqxGF-speed" className="font-medium">
              Speed
            </label>
            <input
              id="mqxGF-speed"
              type="range"
              min={0.5}
              max={2}
              step={0.25}
              value={speed}
              onChange={(e) => setSpeed(Number(e.target.value))}
              disabled={prefersReduced}
              aria-valuetext={`${speed} times`}
              className="h-1.5 w-28 cursor-pointer accent-indigo-600 disabled:cursor-not-allowed disabled:opacity-50 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-white dark:accent-indigo-400 dark:focus-visible:ring-offset-slate-950"
            />
            <output
              htmlFor="mqxGF-speed"
              className="w-10 text-right font-medium tabular-nums text-indigo-600 dark:text-indigo-300"
            >
              {speed}x
            </output>
          </div>
        </div>

        <div
          className="mt-12 space-y-4"
          onMouseEnter={() => setHovered(true)}
          onMouseLeave={() => setHovered(false)}
        >
          <Row
            ariaLabel="Core platform capabilities"
            content={capabilityItems}
            trackStyle={trackStyle(38, dirA)}
            reduced={prefersReduced}
          />
          <Row
            ariaLabel="Additional guarantees"
            content={tagItems}
            trackStyle={trackStyle(28, dirB)}
            reduced={prefersReduced}
          />
        </div>

        <p className="mt-6 text-center text-xs text-slate-500 dark:text-slate-400">
          Hover to pause &middot;{" "}
          {prefersReduced
            ? "Reduced-motion mode: scroll the rows manually"
            : "Keyboard and screen-reader friendly"}
        </p>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →