Installation
npx shadcn@latest add https://tentui.com/r/marquee.json
Usage
Wrap a row of children. The component duplicates the row in place so the animation loops without a visible seam.
import { Marquee } from "@/components/marquee";
const logos = ["Acme", "Vercel", "Linear", "Supabase", "Stripe"];
export function LogoWall() {
return (
<Marquee>
{logos.map((name) => (
<span key={name} className="text-sm font-semibold">
{name}
</span>
))}
</Marquee>
);
}The animation is plain CSS @keyframes — no JavaScript animation loop and no Intersection Observer. The track is duplicated repeat times and translated by (-100% - gap); with at least two copies the loop is seamless.
Patterns
Two opposing rows
Stack two marquees with reverse toggled to get a richer visual without doubling the runtime cost.
<Marquee duration={30}>{children}</Marquee>
<Marquee duration={30} reverse>{children}</Marquee>Vertical orientation
Useful for sidebar testimonials. Wrap in a fixed-height container.
<div className="h-80">
<Marquee vertical duration={50}>{quotes}</Marquee>
</div>Pause on focus
pauseOnHover covers most cases. To pause when any child is keyboard-focused, add focus-within:[animation-play-state:paused] via className on a wrapper, or extend the component.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
reverse | boolean | false | Reverse the scroll direction. |
pauseOnHover | boolean | true | Pause the animation while the marquee is hovered. |
vertical | boolean | false | Scroll vertically — wrap in a fixed-height container. |
repeat | number | 4 | Number of times the children are duplicated. Two is the minimum for a seamless loop; more gives you a denser fill on wide viewports. |
duration | number | 40 | Animation duration in seconds. Lower = faster scroll. |
fade | boolean | true | Apply a fade-out CSS mask at the leading and trailing edges. |
className | string | — | Forwarded to the outer wrapper. |
Accessibility & motion
- Only the first track is in the accessibility tree — the duplicates have
aria-hidden="true"so screen readers don't read the same logos N times. - A
prefers-reduced-motion: reducemedia query disables the animation entirely. Users who opt out of motion still see the content, just stationary.
Performance
The track is one transform animation per copy — GPU-accelerated and cheap. The CSS mask gradient is also free on the GPU. The biggest cost is rendering the children themselves, so keep them lightweight (no heavy shadows or filters per item) on long lists.