VeloxUI
layout·free

Marquee

Seamless infinite scrolling strip with hover slow-down and edge fade.

Next.jsGSAPReact Three FiberTailwindTypeScriptVercel

Installation

terminal
pnpm add gsap @gsap/react

Then copy the component below into src/components/motion/marquee.tsx. It expects @/lib/gsap (plugin registration) and @/lib/utils (cn).

Source

src/components/motion/marquee.tsx
"use client";

import { useRef, type ReactNode } from "react";
import { gsap, useGSAP } from "@/lib/gsap";
import { cn } from "@/lib/utils";

export interface MarqueeProps {
  children: ReactNode;
  className?: string;
  /** Pixels per second. */
  speed?: number;
  reverse?: boolean;
  pauseOnHover?: boolean;
  gap?: number;
}

export function Marquee({
  children,
  className,
  speed = 80,
  reverse = false,
  pauseOnHover = true,
  gap = 48,
}: MarqueeProps) {
  const ref = useRef<HTMLDivElement>(null);
  const track = useRef<HTMLDivElement>(null);

  useGSAP(
    () => {
      const t = track.current;
      if (!t) return;
      const width = t.scrollWidth / 2;
      const tween = gsap.to(t, {
        x: reverse ? 0 : -width,
        duration: width / speed,
        ease: "none",
        repeat: -1,
        ...(reverse ? { startAt: { x: -width } } : {}),
      });
      if (!pauseOnHover) return;
      const el = ref.current!;
      const slow = () => gsap.to(tween, { timeScale: 0, duration: 0.6 });
      const resume = () => gsap.to(tween, { timeScale: 1, duration: 0.6 });
      el.addEventListener("mouseenter", slow);
      el.addEventListener("mouseleave", resume);
      return () => {
        el.removeEventListener("mouseenter", slow);
        el.removeEventListener("mouseleave", resume);
      };
    },
    { scope: ref, dependencies: [speed, reverse, pauseOnHover, gap] }
  );

  return (
    <div
      ref={ref}
      className={cn("overflow-hidden [mask-image:linear-gradient(90deg,transparent,#000_10%,#000_90%,transparent)]", className)}
    >
      <div ref={track} className="flex w-max will-change-transform" style={{ gap }}>
        <div className="flex shrink-0 items-center" style={{ gap }}>
          {children}
        </div>
        <div className="flex shrink-0 items-center" style={{ gap }} aria-hidden>
          {children}
        </div>
      </div>
    </div>
  );
}