liten

Marquee

A seamless infinite scroller for logos, testimonials, or any row of content - GPU-cheap, pausing on hover, with faded edges.

The content is rendered twice and the track translates by exactly -50%, so the loop is perfectly continuous. Only transform animates, off the main thread, so it stays smooth. It pauses on hover and stops under reduced motion.

Installation

Complete the shared Setup first, then copy the component into components/ui/marquee.tsx.

components/ui/marquee.tsx
'use client';import * as React from 'react';import { cn } from '@/lib/cn';export type MarqueeProps = {  children: React.ReactNode;  duration?: number;  direction?: 'left' | 'right';  gap?: number;  pauseOnHover?: boolean;  fade?: boolean;  className?: string;};export function Marquee({  children,  duration = 30,  direction = 'left',  gap = 40,  pauseOnHover = true,  fade = true,  className,}: MarqueeProps) {  const mask = fade    ? 'linear-gradient(to right, transparent, #000 10%, #000 90%, transparent)'    : undefined;  return (    <div      data-pause={pauseOnHover ? 'true' : 'false'}      className={cn('marquee group relative w-full overflow-hidden', className)}      style={{ maskImage: mask, WebkitMaskImage: mask }}    >      <div        className="marquee-track flex w-max flex-nowrap"        data-direction={direction}        style={{ gap, ['--marquee-duration' as string]: `${duration}s` }}      >        <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>  );}

Add the animation to your global.css.

global.css
@keyframes marquee-x {
  to {
    transform: translateX(-50%);
  }
}
@keyframes marquee-x-reverse {
  to {
    transform: translateX(50%);
  }
}
.marquee-track {
  animation: marquee-x var(--marquee-duration, 30s) linear infinite;
}
.marquee-track[data-direction='right'] {
  animation-name: marquee-x-reverse;
}
.marquee[data-pause='true']:hover .marquee-track {
  animation-play-state: paused;
}
@media (prefers-reduced-motion: reduce) {
  .marquee-track {
    animation: none !important;
  }
}

Usage

Example.tsx
import { Marquee } from '@/components/ui/marquee';

export default function Example() {
  return (
    <Marquee>
      <span>Next.js</span>
      <span>React</span>
      <span>TypeScript</span>
    </Marquee>
  );
}

Examples

Two rows scrolling in opposite directions.

Props

PropTypeDefaultDescription
childrenReact.ReactNode-The content to scroll.
durationnumber30Seconds for one full loop.
direction"left" | "right""left"Scroll direction.
gapnumber40Gap between items, in px.
pauseOnHoverbooleantruePause while hovered.
fadebooleantrueFade the left and right edges.
classNamestring-Forwarded to the root.
On this page0%