# Marquee

> Infinite horizontal or vertical scrolling strip with pause on hover, reverse, speed control and edge fade.

- Author: Atelier UI
- License: MIT
- Page: https://chromotion.dev/en/components/marquee
- Site: Atelier UI (https://chromotion.dev)

## Install

```bash
npx shadcn@latest add https://chromotion.dev/r/marquee.json
```

Stack: React + TypeScript + Tailwind CSS v4, shadcn/ui conventions. `cn` comes from `@/lib/utils`.

## Dependencies

No extra dependencies.

## Usage

```tsx title="components/marquee-demo.tsx"
import { Marquee } from "@/components/marquee"

const reviews = [
  { name: "Ada Lovelace", handle: "@ada", body: "The cleanest component set I have shipped with. Zero config, all polish.", tone: "bg-orange-500/15 text-orange-700 dark:text-orange-300" },
  { name: "Linus Park", handle: "@linus", body: "Dropped it into our dashboard and the team thought we hired a designer.", tone: "bg-sky-500/15 text-sky-700 dark:text-sky-300" },
  { name: "Mei Tanaka", handle: "@mei", body: "Accessible by default. Keyboard, focus rings and reduced motion all just work.", tone: "bg-emerald-500/15 text-emerald-700 dark:text-emerald-300" },
  { name: "Omar Haddad", handle: "@omar", body: "Light and dark both look intentional. That almost never happens.", tone: "bg-violet-500/15 text-violet-700 dark:text-violet-300" },
  { name: "Sofia Rossi", handle: "@sofia", body: "Copy, paste, ship. I rebuilt our landing page in an afternoon.", tone: "bg-rose-500/15 text-rose-700 dark:text-rose-300" },
  { name: "Jonas Weber", handle: "@jonas", body: "Tiny API surface and sensible defaults. Exactly what I want from a library.", tone: "bg-amber-500/15 text-amber-700 dark:text-amber-300" },
]

function ReviewCard({ name, handle, body, tone }: (typeof reviews)[number]) {
  return (
    <figure className="w-64 shrink-0 rounded-2xl border bg-card p-4 text-card-foreground shadow-sm">
      <div className="flex items-center gap-2.5">
        <span className={`flex size-8 items-center justify-center rounded-full text-xs font-semibold ${tone}`}>
          {name
            .split(" ")
            .map((part) => part[0])
            .join("")}
        </span>
        <figcaption className="min-w-0">
          <p className="truncate text-sm font-medium">{name}</p>
          <p className="truncate text-xs text-muted-foreground">{handle}</p>
        </figcaption>
      </div>
      <blockquote className="mt-3 text-sm leading-relaxed text-muted-foreground">{body}</blockquote>
    </figure>
  )
}

export default function MarqueeDemo() {
  const first = reviews.slice(0, 3)
  const second = reviews.slice(3)

  return (
    <div className="flex w-full max-w-3xl flex-col gap-4 py-4">
      <Marquee pauseOnHover duration={36}>
        {[...first, ...second].map((review) => (
          <ReviewCard key={review.handle} {...review} />
        ))}
      </Marquee>
      <Marquee pauseOnHover reverse duration={44}>
        {[...second, ...first].map((review) => (
          <ReviewCard key={review.handle} {...review} />
        ))}
      </Marquee>
      <p className="text-center text-xs text-muted-foreground">Hover or focus a row to pause it.</p>
    </div>
  )
}
```

## Source

```tsx title="components/marquee.tsx"
import * as React from "react"

import { cn } from "@/lib/utils"

const keyframes = `@keyframes atelier-marquee-x{to{transform:translateX(calc(-100% - var(--marquee-gap)))}}@keyframes atelier-marquee-y{to{transform:translateY(calc(-100% - var(--marquee-gap)))}}`

export interface MarqueeProps extends React.ComponentProps<"div"> {
  /** Scroll vertically instead of horizontally */
  vertical?: boolean
  /** Reverse the scroll direction */
  reverse?: boolean
  /** Pause while hovered or focused */
  pauseOnHover?: boolean
  /** Seconds for one full loop at speed 1 */
  duration?: number
  /** Speed multiplier, 2 is twice as fast */
  speed?: number
  /** Gap between items, any CSS length */
  gap?: string
  /** How many times the content is repeated to fill the strip */
  repeat?: number
  /** Fade the edges with a mask */
  fade?: boolean
  /** Size of the edge fade, in percent of the strip */
  fadeSize?: number
}

// The children are repeated `repeat` times; each copy slides by exactly its own
// length (plus one gap), so the loop is seamless. The animation is built from
// longhand classes so hover can flip `animation-play-state` and reduced motion
// can switch it off with `animate-none`.
export function Marquee({
  vertical = false,
  reverse = false,
  pauseOnHover = true,
  duration = 40,
  speed = 1,
  gap = "1rem",
  repeat = 4,
  fade = true,
  fadeSize = 12,
  className,
  style,
  children,
  ...props
}: MarqueeProps) {
  const dir = vertical ? "to bottom" : "to right"
  const mask = fade
    ? `linear-gradient(${dir}, transparent, #000 ${fadeSize}%, #000 ${100 - fadeSize}%, transparent)`
    : undefined

  return (
    <div
      className={cn("group flex gap-(--marquee-gap) overflow-hidden", vertical && "flex-col", className)}
      style={
        {
          "--marquee-gap": gap,
          "--marquee-duration": `${duration / Math.max(speed, 0.01)}s`,
          maskImage: mask,
          WebkitMaskImage: mask,
          ...style,
        } as React.CSSProperties
      }
      {...props}
    >
      <style href="atelier-marquee" precedence="default">
        {keyframes}
      </style>
      {Array.from({ length: Math.max(repeat, 2) }, (_, i) => (
        <div
          key={i}
          aria-hidden={i > 0 || undefined}
          className={cn(
            "flex shrink-0 justify-around gap-(--marquee-gap)",
            vertical ? "flex-col" : "flex-row",
            vertical ? "[animation-name:atelier-marquee-y]" : "[animation-name:atelier-marquee-x]",
            "[animation-duration:var(--marquee-duration)] [animation-timing-function:linear] [animation-iteration-count:infinite]",
            reverse && "[animation-direction:reverse]",
            pauseOnHover && "group-hover:[animation-play-state:paused] group-focus-within:[animation-play-state:paused]",
            "motion-reduce:animate-none"
          )}
        >
          {children}
        </div>
      ))}
    </div>
  )
}
```
