# Marquee

CSS-only infinite marquee with faded edges. It pauses on hover and stops for reduced motion.

**Category:** Display  
**Exports:** `Marquee`  
**npm dependencies:** none (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/marquee.json
```
The file lands at `components/ui/marquee.tsx`; import from `@/components/ui/marquee`.

## API — Marquee
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `duration` | `number` | `30` | Seconds per loop. |
| `reverse` | `boolean` | `false` |  |
| `vertical` | `boolean` | `false` |  |
| `pauseOnHover` | `boolean` | `true` |  |
| `gap` | `string` | `"2rem"` |  |

## Example — Marquee

```tsx
import { Marquee } from "@/components/ui/marquee";

const quotes = [
  ["Ana", "The slider alone sold me."],
  ["Ravi", "Finally, motion that feels physical."],
  ["Mei", "Dropped it into Next.js in two minutes."],
  ["Omar", "The ember accent is so tasteful."],
  ["Lena", "Reduced motion support out of the box."],
];

export default function MarqueeDemo({ duration = 30, gap = "2rem", pauseOnHover = true }: { duration?: number; gap?: string; pauseOnHover?: boolean }) {
  return (
    <div className="grid w-full max-w-2xl gap-4">
      {[false, true].map((reverse) => (
        <Marquee key={String(reverse)} reverse={reverse} duration={duration} gap={gap} pauseOnHover={pauseOnHover}>
          {quotes.map(([name, q]) => (
            <figure key={name} className="w-64 rounded-xl border bg-background p-4">
              <blockquote className="text-sm">“{q}”</blockquote>
              <figcaption className="mt-2 font-mono text-xs text-muted-foreground">{name}</figcaption>
            </figure>
          ))}
        </Marquee>
      ))}
    </div>
  );
}
```

## Source — components/ui/marquee.tsx

```tsx
import * as React from "react";
import { cn } from "@/lib/utils";

export type MarqueeProps = {
  /** Seconds per full loop. */
  duration?: number;
  reverse?: boolean;
  vertical?: boolean;
  pauseOnHover?: boolean;
  gap?: string;
  /** Copies of the content; raise it if short content leaves a gap. */
  repeat?: number;
  className?: string;
  children: React.ReactNode;
};

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

/** CSS-only marquee: no JavaScript runs per frame. */
export function Marquee({
  duration = 30,
  reverse = false,
  vertical = false,
  pauseOnHover = true,
  gap = "2rem",
  repeat = 2,
  className,
  children,
}: MarqueeProps) {
  return (
    <div
      className={cn(
        "group relative flex overflow-hidden",
        vertical
          ? "flex-col [mask-image:linear-gradient(to_bottom,transparent,black_12%,black_88%,transparent)]"
          : "flex-row [mask-image:linear-gradient(to_right,transparent,black_12%,black_88%,transparent)]",
        className,
      )}
      style={{ "--marquee-duration": `${duration}s`, "--marquee-gap": gap, gap } as React.CSSProperties}
    >
      <style href="charcoal-marquee" precedence="default">
        {css}
      </style>
      {Array.from({ length: repeat }, (_, i) => (
        <div
          key={i}
          aria-hidden={i > 0 || undefined}
          className={cn(
            "flex shrink-0 justify-around motion-reduce:[animation-play-state:paused]",
            vertical
              ? "flex-col [animation:charcoal-marquee-y_var(--marquee-duration)_linear_infinite]"
              : "flex-row [animation:charcoal-marquee-x_var(--marquee-duration)_linear_infinite]",
            reverse && "[animation-direction:reverse]",
            pauseOnHover && "group-hover:[animation-play-state:paused]",
          )}
          style={{ gap }}
        >
          {children}
        </div>
      ))}
    </div>
  );
}
```