Skip to content
jjswnth/ui

Layout

Marquee

An infinite, gap-aware scroller with edge fading and pause on hover.

Dropped it in and shipped the same day.
Aria M. · Design engineer
The only component set that matched our brand.
Kel D. · Founder
Zero runtime surprises, which is the point.
Ravi S. · Frontend lead
Motion defaults are genuinely tasteful.
Juno P. · Product designer
Dropped it in and shipped the same day.
Aria M. · Design engineer
The only component set that matched our brand.
Kel D. · Founder
Zero runtime surprises, which is the point.
Ravi S. · Frontend lead
Motion defaults are genuinely tasteful.
Juno P. · Product designer
Design engineer
Founder
Frontend lead
Product designer
Design engineer
Founder
Frontend lead
Product designer

Installation

  1. 1. Install the dependencies.

    terminal
    npm install clsx tailwind-merge
  2. 2. Add the cn helper, if you don't already have it.

    lib/utils.ts
    import { clsx, type ClassValue } from "clsx";
    import { twMerge } from "tailwind-merge";
    
    export function cn(...inputs: ClassValue[]) {
      return twMerge(clsx(inputs));
    }
  3. 3. Copy the component into your project.

    components/ui/marquee.tsx
    "use client";
    
    import { Children, type ReactNode } from "react";
    import { cn } from "@/lib/utils";
    
    type MarqueeProps = {
      children: ReactNode;
      className?: string;
      /** Seconds for one full loop. */
      duration?: number;
      /** Scroll right-to-left by default; flip to reverse. */
      reverse?: boolean;
      /** Pause the loop while the cursor is over it. */
      pauseOnHover?: boolean;
      /** Fade the leading and trailing edges into the background. */
      fade?: boolean;
      /** Gap between items, as a CSS length. */
      gap?: string;
    };
    
    export function Marquee({
      children,
      className,
      duration = 32,
      reverse = false,
      pauseOnHover = true,
      fade = true,
      gap = "1rem",
    }: MarqueeProps) {
      const items = Children.toArray(children);
    
      return (
        <div
          className={cn("group relative w-full overflow-hidden", className)}
          style={
            fade
              ? {
                  maskImage:
                    "linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent)",
                  WebkitMaskImage:
                    "linear-gradient(90deg, transparent, #000 12%, #000 88%, transparent)",
                }
              : undefined
          }
        >
          <div
            className={cn(
              "flex w-max motion-safe:animate-marquee",
              reverse && "[animation-direction:reverse]",
              pauseOnHover && "group-hover:[animation-play-state:paused]",
            )}
            style={
              {
                gap,
                "--marquee-duration": `${duration}s`,
                "--marquee-gap": gap,
              } as React.CSSProperties
            }
          >
            {[...items, ...items].map((item, index) => (
              <div key={index} className="shrink-0">
                {item}
              </div>
            ))}
          </div>
        </div>
      );
    }

Props

PropTypeDefaultDescription
childrenReactNodeItems to loop. They are duplicated once for a seamless cycle.
durationnumber32Seconds for one full loop.
reversebooleanfalseScroll left-to-right instead.
pauseOnHoverbooleantruePause the loop while hovered.
fadebooleantrueFade the leading and trailing edges.
gapstring"1rem"Gap between items, as a CSS length.