Infinite Marquee

Seamless auto-scrolling strip for logos, testimonials, or tags — pauses on hover, directional, and gradient-masked edges

marqueecarousellogosinfinitescrolling
Preview

Source Code

"use client";

import { Children, ReactNode, useId } from "react";

interface InfiniteMarqueeProps {
  children: ReactNode;
  direction?: "left" | "right";
  speed?: number;
  pauseOnHover?: boolean;
  gap?: number;
  className?: string;
}

export default function InfiniteMarquee({
  children,
  direction = "left",
  speed = 30,
  pauseOnHover = true,
  gap = 48,
  className = "",
}: InfiniteMarqueeProps) {
  const uid = useId().replace(/:/g, "");
  const animationName = `marquee-${uid}`;
  const items = Children.toArray(children);

  // Direction is achieved by flipping the keyframes' end transform sign.
  const endX = direction === "left" ? "-50%" : "0%";
  const startX = direction === "left" ? "0%" : "-50%";

  const styles = `
    @keyframes ${animationName} {
      from { transform: translateX(${startX}); }
      to   { transform: translateX(${endX}); }
    }
    .${animationName}-track {
      display: flex;
      width: max-content;
      flex-shrink: 0;
      gap: ${gap}px;
      padding-right: ${gap}px;
      animation: ${animationName} ${speed}s linear infinite;
      will-change: transform;
    }
    .${animationName}-root:hover .${animationName}-track {
      animation-play-state: ${pauseOnHover ? "paused" : "running"};
    }
    @media (prefers-reduced-motion: reduce) {
      .${animationName}-track { animation-duration: ${speed * 4}s; }
    }
  `;

  return (
    <div
      className={`${animationName}-root relative overflow-hidden ${className}`}
      style={{
        maskImage:
          "linear-gradient(to right, transparent, black 12%, black 88%, transparent)",
        WebkitMaskImage:
          "linear-gradient(to right, transparent, black 12%, black 88%, transparent)",
      }}
    >
      <style dangerouslySetInnerHTML={{ __html: styles }} />
      <div className={`${animationName}-track`}>
        {items.map((child, i) => (
          <div key={`a-${i}`} className="flex shrink-0 items-center">
            {child}
          </div>
        ))}
        {items.map((child, i) => (
          <div key={`b-${i}`} aria-hidden className="flex shrink-0 items-center">
            {child}
          </div>
        ))}
      </div>
    </div>
  );
}