A row of content that runs past without a seam, for the places where a list is longer than the space given to it and the point is the list rather than any one entry. Reach for it for a wall of customer or partner logos, a strip of press mentions, "as seen in" or integration badges, the speaker line-up on a conference page, a status or incident band along the top of a dashboard, a notice scrolling across a lobby or kiosk display, a ticker of recent signups, reviews or trades, a tag cloud that will not fit, or a screenshot reel on a landing page. Common asks it answers: "marquee react", "infinite scrolling logos react", "seamless infinite scroll carousel", "logo cloud auto scroll", "css marquee without jank", "ticker tape component react", "news ticker react", "auto scrolling text banner", "continuous horizontal scroll animation", "shadcn marquee", "logo marquee tailwind", "scrolling partner logos", "infinite loop animation react", "magicui marquee", "react-fast-marquee", "react-marquee-slider", "marquee pause on hover", "marquee accessibility", "prefers-reduced-motion marquee", "how many copies for a seamless marquee", "marquee jumps at the loop", "marquee speed depends on how many items", "my marquee duplicates screen reader output", "duplicate links in marquee tab order", "WCAG pause stop hide marquee", "横スクロールする帯 react", "ロゴを無限に流す", "繋ぎ目のないマーキー", "ニュースティッカー 実装", "ホバーで止まる流れる帯", "流れる帯 アクセシビリティ". **The seam is the whole problem, and the only way to hide it is to duplicate the content.** One copy sliding left eventually drags its own end into view, so the content is rendered several times over and the track travels exactly the distance from one copy to the next — at which point every copy stands where the one before it stood and the jump back to the start has nothing to give it away. Measured on a Web Animations loop, the computed transform at the end of an iteration is identical to the transform at the start. **The duplicates are `aria-hidden` and `inert`, and both are needed.** Measured in a browser, `aria-hidden` on its own leaves a clone's links and buttons focusable, so a wall of three logo links rendered in four copies hands a keyboard user twelve tab stops, nine of which lead somewhere a screen reader has been told is not there. `inert` takes them out of the tab order as well, and keeps a clone from swallowing a click meant for the real thing. Only the first copy is in the accessibility tree, so the list is read once. **Speed is pixels per second, not a duration**, because a duration is not a speed: three logos and thirty logos both set to "20s" travel at a tenth of each other's pace, so every addition to the list silently retunes the animation. Here the duration is computed from the distance that was measured, and adding a logo does not change how fast the band reads. **How many copies it takes is arithmetic, and the term everybody drops is the gap.** A loop moves the track by one step, so the copies have to reach `step + box width`; `n` copies reach `n * step - gap`, not `n * step`, because the gap trails every copy but the last. That makes the count `ceil((box + gap) / step) + 1`. The version without the gap is right almost everywhere — swept over 27 combinations of gap and box width it was short in one, and in that one (a 40px gap, a 300px box, a 316.44px step) it left 23.56px of emptiness at the trailing edge at the moment of fullest travel, once per loop, forever. The gap is never asked for: it is the measured step minus the measured copy width, so a gap set from anywhere still agrees with the layout. **The distance is read as a difference between two rectangles, never from `offsetLeft` or `offsetWidth`.** Those are rounded integers, and taking the step from them came out 0.5px and 0.4375px wrong in the two layouts measured — half a pixel of jump at the seam on every loop, for nothing. A difference also includes the gap without asking about it, and is immune to the transform the track is already under: the same two copies measured 268.4375 apart both at rest and mid-animation, while the absolute left of one of them had moved from 0 to −109.56. **The track is out of flow, with one hidden copy left in flow to size the box, and that is not a stylistic choice.** The track is as wide as the copy count, so a box taking its width from its content gets wider every time the count goes up, and a wider box needs a higher count: measured inside an `inline-flex` parent that ran 210px → 444px → 912px → 1848px as the count doubled, with nothing to stop it. With the track absolutely positioned and one copy's worth of content in flow behind it, the width came out identical at 1, 2, 4, 8 and 16 copies in all four parent layouts tried. The same hidden copy gives the box its height, so the band is the right height in the server's HTML rather than after a measurement — and it is intrinsically sized, because a spacer narrow enough to squeeze its items wraps them and reported 60px of height where the copies were 24px. **The box clips without becoming something that can be scrolled.** `overflow: hidden` makes a scroll container, and a browser scrolls one to reveal a focused descendant — measured, tabbing to the last link of the visible copy in a box narrower than that copy scrolled the box to 284 and left it there, sliding the whole band sideways for good while the animation carried on from the wrong place. `overflow: clip` clips identically without the scroll container: the same focus left it at 0 and did not scroll the page either. The class stays underneath as the fallback for an engine that drops the declaration. **Under `prefers-reduced-motion: reduce` this is a different component, not a stopped one.** A stopped marquee is a list with its end cut off: measured on a 300px box, 233px of the content sat outside it. Reduce gets a plain wrapping list instead, which measured 0px of overflow with every item inside the box. The fallback has to be a different tree rather than a flag because of the layout — the copies must refuse to shrink for the track to be wider than the box, and refusing to shrink is exactly what stops them wrapping. The media query is subscribed to, so turning the setting on mid-visit stops the band without a reload. **Hover and focus stop it, and the button is the part that matters.** Moving content that runs for more than five seconds needs a way to stop it (WCAG 2.2.2), and hover is not a way for anyone without a mouse, so the pause button is on by default, reachable by keyboard whether or not it is visible, and visible from the start on a device with no hover to reveal it with. Hover is ignored for touch pointers on purpose: a tap fires pointerenter with no pointer left to leave, and a band stopped for the rest of a visit because a thumb brushed a logo is worse than one that never stopped. Pausing holds the content where it is and resuming carries on from there rather than from the top — measured resuming at 806ms from a pause taken at 700ms — and anything that changes the distance (a resize, a web font arriving, a logo added) rebuilds the animation at the phase the old one had reached, because a rebuilt animation otherwise starts at zero and snaps the band back: measured, that put the track at translateX(0) where it had been at −134.219, while carrying the phase across reproduced −134.219 exactly. It animates through the Web Animations API rather than CSS keyframes, because the distance is a measurement and keyframes carrying a measured distance would have to be generated per instance — which a registry component cannot do to someone else's stylesheet. The animation is declarative either way, so this is not a `requestAnimationFrame` loop. `data-state` is "running" or "paused" for styling the stopped state, `paused` and `onPausedChange` hand that state to the caller, `direction` runs it rightwards, and `marqueeCopyCount` is exported so the arithmetic can be reused or checked. Content is cloned, so a child that holds state or costs something to render is rendered several times over; logos, names and short text are the intended cargo. Theme tokens only, no hardcoded colours, no dependencies beyond React and lucide-react for the two icons. **Official shadcn/ui has nothing here, and the measurement is not close.** Fetching every entry in its registry today — 65 listed, 62 fetchable, code-block and questionnaire and trace 404 on both style tracks — and searching all 223,287 bytes of it, the strings marquee, ticker, scrollAmount, prefers-reduced-motion, keyframes, animation and playbackRate are every one of them a zero hit. Official transitions between states 47 times and animates nothing at all: its single `translateX` is the progress bar holding its indicator at a fixed offset. Within pulld the nearest neighbours are tab-scroller and scroll-shadow, which both react to scrolling rather than moving by themselves, and announcement-bar, which is a band that holds still; sortable-list and swipe-actions move under a pointer rather than on their own. Nothing else in the registry runs continuously, and nothing else duplicates its children.
pnpm dlx shadcn@latest add "https://pulld.pages.dev/r/marquee.json"