Marquee

A seamless scrolling row or column, in either direction, that pauses on hover.

Loading preview

Installation

npx shadcn@latest add @loomui/marquee

Usage

import { Marquee } from "@/components/ui/marquee"
<Marquee pauseOnHover duration={28}>
  {quotes.map((quote) => (
    <figure key={quote} className="w-64 shrink-0 rounded-xl border p-4">
      <blockquote className="text-sm">{quote}</blockquote>
    </figure>
  ))}
</Marquee>

The children are rendered twice. The track travels exactly one copy plus one gap, then snaps back, and because the second copy is sitting where the first one started the seam is invisible. Copies after the first are aria-hidden.

fade softens both ends. Pass a CSS length to size it:

<Marquee fade="4rem">{/* ... */}</Marquee>

The fade is a mask, not a gradient overlay. It works over an image, a gradient, or any surface that is not a flat background colour.

Props

PropTypeDefaultDescription
verticalbooleanfalseTravel bottom to top instead of right to left.
reversebooleanfalseFlip the direction of travel.
pauseOnHoverbooleanfalseStop while the pointer is over the track.
repeatnumber2Copies of the children. Two is the minimum.
durationnumber40Seconds for one full pass of a single copy.
gapstring"1rem"Gap between items, as a CSS length.
pausedbooleanfalseRender the row static.
fadeboolean | stringfalseFade both edges out. A string sizes it.
classNamestringnoneMerged onto the track.

Any other <div> prop is forwarded.

Why linear

A marquee is the one place linear is correct. Any easing curve makes the loop visibly speed up and slow down once per pass, which points straight at the seam you are trying to hide.

Accessibility

Motion stops under prefers-reduced-motion, leaving the first copy readable in place. Duplicated copies are aria-hidden, so the content is announced once.