# Marquee

An accessible React marquee for Next.js and Tailwind CSS: an endless, seamless row of logos or cards that pauses on hover and stops with reduced motion.

Source: https://ui.fewtone.com/components/marquee

## Installation

```bash
npx shadcn@latest add https://ui.fewtone.com/r/marquee.json
```

## Usage

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

export function Logos() {
  return (
    <Marquee>
      <span>Northwind</span>
      <span>Atlas</span>
      <span>Vertex</span>
    </Marquee>
  )
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `direction` | `"left" \| "right" \| "up" \| "down"` | `"left"` | Which way the items travel. Up and down need a height (className="h-72"). |
| `speed` | `number` | `40` | Pixels a second. |
| `pauseOnHover` | `boolean` | `true` | Slow to a stop while the pointer is over it, and pick up again on leaving. |
| `paused` | `boolean` | `false` | Stops it, slowing down the same way. Wire it to a pause control when the marquee runs beside other content. |
| `gap` | `number \| string` | `16px (gap-4)` | Space between items and between the copies: a CSS length, or a number of pixels. |
| `fade` | `boolean` | `true` | Fades the items out at both ends. |

**When to use it:** Use Marquee for a band that moves on its own: customer logos, a wall of testimonials, a ticker of short updates. Keep the items short and give users a way to stop it (paused) when it runs beside other content. Use direction="up" or "down" with a height for a vertical column.
**Built on:** Web Animations API.
**npm packages (installed by the CLI):** motion@^14.0.0.

## Examples

### Logos and cards

The items repeat until they fill the row, so the loop never shows a seam. Hover a row and it slows to a stop rather than freezing; Pause stops both the same way.

```tsx
"use client";

import { Card, CardContent } from "@/components/ui/card";
import { Marquee } from "@/components/ui/marquee";
import { Switch } from "@/components/ui/switch";
import { cn } from "@/lib/utils";
import { useId, useState } from "react";

/** Wordmarks set in type, each with its own weight and spacing, the way real logos differ */
const logos = [
  { name: "Northwind", style: "font-semibold tracking-tight" },
  { name: "ATLAS", style: "font-semibold tracking-wide" },
  { name: "orbit", style: "font-mono font-medium" },
  { name: "Kite & Co", style: "font-semibold italic" },
  { name: "Vertex", style: "font-bold tracking-tight" },
  { name: "harbor", style: "font-medium" },
  { name: "Quill", style: "font-semibold italic tracking-tight" },
  { name: "LUMEN", style: "font-medium tracking-wide" },
];

const quotes = [
  { quote: "Setup took an afternoon, not a sprint.", name: "Maya Chen", role: "Design lead" },
  {
    quote: "It looked right in our theme on the first try.",
    name: "Tomás Rivera",
    role: "Frontend engineer",
  },
  { quote: "The motion is quiet. Nothing shows off.", name: "Priya Nair", role: "Product designer" },
  { quote: "Keyboard support was already there.", name: "Jonas Berg", role: "Accessibility consultant" },
  { quote: "We deleted half our custom CSS.", name: "Ava Thompson", role: "Founder" },
];

/*
 * A row of wordmarks travelling left and a row of small quote cards travelling right, named underneath. Hover a
 * row and it slows to a stop; Pause, at the top right, stops both the same way (a pause control for moving
 * content).
 */
export default function MarqueeRows() {
  const id = useId();
  const [paused, setPaused] = useState(false);

  return (
    <div className="flex w-full min-w-0 flex-col gap-6 self-stretch">
      <div className="-mt-6 -mr-6 flex items-center justify-end gap-2">
        <label htmlFor={`${id}-pause`} className="text-sm text-muted-foreground">
          Pause
        </label>
        <Switch id={`${id}-pause`} size="sm" checked={paused} onCheckedChange={setPaused} />
      </div>

      <div className="flex flex-1 flex-col justify-center gap-6">
        <div className="flex flex-col items-center gap-2">
          {/* The items carry the space between them, so gap is 0 */}
          <Marquee paused={paused} gap={0} className="w-full">
            {logos.map((logo) => (
              <span key={logo.name} className={cn("px-6 text-base text-muted-foreground", logo.style)}>
                {logo.name}
              </span>
            ))}
          </Marquee>
          <span className="text-xs text-muted-foreground">Logos</span>
        </div>
        <div className="flex flex-col items-center gap-2">
          {/* Room above and below, so the cards' shadows aren't clipped */}
          <Marquee paused={paused} direction="right" speed={30} className="w-full py-2">
            {quotes.map((item) => (
              <Card key={item.name} size="sm" className="w-64" asChild>
                <figure>
                  <CardContent className="flex flex-col gap-3">
                    <blockquote className="text-sm">“{item.quote}”</blockquote>
                    <figcaption className="text-xs">
                      <span className="font-medium">{item.name}</span>
                      <span className="text-muted-foreground"> · {item.role}</span>
                    </figcaption>
                  </CardContent>
                </figure>
              </Card>
            ))}
          </Marquee>
          <span className="text-xs text-muted-foreground">Cards, the other way</span>
        </div>
      </div>

      <div aria-hidden className="-mb-6 h-(--size-control-sm)" />
    </div>
  );
}
```

### Vertical

Up and down need a height on the marquee. Two columns going opposite ways make a wall of quotes.

```tsx
import { Card, CardContent } from "@/components/ui/card";
import { Marquee } from "@/components/ui/marquee";

const quotes = [
  { quote: "Setup took an afternoon, not a sprint.", name: "Maya Chen" },
  { quote: "It looked right in our theme on the first try.", name: "Tomás Rivera" },
  { quote: "The motion is quiet. Nothing shows off.", name: "Priya Nair" },
  { quote: "Keyboard support was already there.", name: "Jonas Berg" },
  { quote: "We deleted half our custom CSS.", name: "Ava Thompson" },
  { quote: "Dark mode just worked.", name: "Leo Park" },
];

function Quote({ quote, name }: { quote: string; name: string }) {
  return (
    <Card size="sm" asChild>
      <figure>
        <CardContent className="flex flex-col gap-3">
          <blockquote className="text-sm">“{quote}”</blockquote>
          <figcaption className="text-xs text-muted-foreground">{name}</figcaption>
        </CardContent>
      </figure>
    </Card>
  );
}

/*
 * Two columns of quotes, one travelling up and one down. A vertical marquee needs a height; the ends fade
 * top and bottom.
 */
export default function MarqueeVertical() {
  return (
    <div className="grid w-full max-w-md min-w-0 grid-cols-2 gap-4">
      {/* Room at the sides, so the cards' shadows aren't clipped */}
      <Marquee direction="up" speed={24} className="h-72 px-2">
        {quotes.map((item) => (
          <Quote key={item.name} {...item} />
        ))}
      </Marquee>
      <Marquee direction="down" speed={24} className="h-72 px-2">
        {[...quotes].reverse().map((item) => (
          <Quote key={item.name} {...item} />
        ))}
      </Marquee>
    </div>
  );
}
```

## Related

- [Card](https://ui.fewtone.com/components/card.md): A React card for Next.js and Tailwind CSS: a surface for grouped content with header, action, content and footer parts, a small size and a link style.
- [Text Effects](https://ui.fewtone.com/components/text-effects.md): Accessible React text effects for Next.js and Tailwind CSS: a shimmer, a word-by-word reveal and a typewriter, all real text and off with reduced motion.
- [Number Ticker](https://ui.fewtone.com/components/number-ticker.md): An accessible React number ticker for Next.js and Tailwind CSS: digits roll like an odometer and glide as the number changes length, with Intl formatting.
