Marquee
A strip that loops its items only once CSS scroll-driven animations detect they overflow
Installation
pnpm dlx shadcn@latest add @alexandrejoly/marqueeUsage
import { Marquee, MarqueeContent, MarqueeItem, MarqueePause, MarqueeViewport,} from "@/components/ui/marquee";<Marquee> <MarqueePause /> <MarqueeViewport> <MarqueeContent> <MarqueeItem>Item 1</MarqueeItem> <MarqueeItem>Item 2</MarqueeItem> </MarqueeContent> </MarqueeViewport></Marquee>Examples
Pause on hover
A slower track, from [--marquee-duration:60s], pauses under the pointer so longer items can be read.
Reverse
Two tracks in one marquee, the second with reverse, pass each other.
Vertical
A vertical track, faded at the top and bottom, runs on a shorter cycle from [--marquee-duration:12s].
Content that fits
Items narrower than the viewport stand still: no clone, no fade, no pause button. CSS scroll-driven animations detect the overflow.
Centered when it fits
items-center-safe on MarqueeViewport centers a track that fits. Once the track overflows, safe aligns it to the start again, so the loop runs as usual.
Accessibility
Screen readers and the Tab key skip the second copy of each track, so they meet every item once. A pointer can still click it.
MarqueePause lets people stop the motion: it stops every track where it is, and its name switches between Pause and Play. Place it before MarqueeViewport so Tab reaches it first.
Keyboard focus stops the tracks and brings the focused item fully into view. A pointer press, or hovering with pauseOnHover, pauses them only while it lasts. Neither replaces MarqueePause.
Under prefers-reduced-motion: reduce the tracks stand still and MarqueePause hides.
Meets WCAG 2.2: 2.2.2 Pause, Stop, Hide (opens in a new tab) · 2.4.3 Focus Order (opens in a new tab) · 2.4.11 Focus Not Obscured (Minimum) (opens in a new tab) · 4.1.2 Name, Role, Value (opens in a new tab)
API reference
| Prop | Type | Default |
|---|---|---|
defaultPausedThe pause state on first render, when you don’t control it. Default: | boolean | false |
pausedThe pause state, when you control it. | boolean | None |
onPausedChangeCalled when | (paused: boolean) => void | None |
MarqueeViewport
Clips the tracks. While its items fit, it stands still with no clone, in browsers with CSS scroll-driven animations. Add shadcn’s scroll-fade-x, or scroll-fade-y on a vertical track, to fade its edges while the items slide: it sets --scroll-fade-reveal: 0px, so both fades show although it never scrolls. Its scroll-padding: 50% centers the item keyboard focus lands on, so keep it. It clips on both axes, since detecting the overflow needs a scroll container, so it cuts the focus ring of a link inside: give the ring room with py-1, or px-1 around a vertical track.
Takes every prop of div.
MarqueeContent
The track. It renders its child elements twice and a CSS keyframe slides it by one copy per cycle. Bare text is dropped, so wrap it in MarqueeItem. Under reduced motion it stays on the first copy.
Takes every prop of div.
| Prop or variable | Type | Default |
|---|---|---|
orientation
Default: | "horizontal" | "vertical" | "horizontal" |
reverseSlides the other way. Horizontal only. Default: | boolean | false |
pauseOnHoverPauses the track while a pointer is over the viewport. Default: | boolean | false |
--marquee-durationOne full cycle, set from a class: Default: | CSS time | 40s |
MarqueeItem
One item of the track. Its padding spaces the items and the loop’s seam alike: px-4 on a horizontal track, py-2 on a vertical one. Override it with a class such as px-2.
Takes every prop of div.
MarqueePause
Pauses and resumes every track. It is a ghost icon button by default; pass variant or size to change it. It hides whenever the tracks stand still: under reduced motion, and while the items fit in browsers with CSS scroll-driven animations.
Takes every prop of shadcn Button (opens in a new tab).