Marquee
ui-beats/marqueeFreeComponent
The Marquee component scrolls its children in a seamless, infinite loop, with pause-on-hover and faded edges.
Install it
npx shadcn@latest add https://uibeats.com/r/marquee.jsonSource
1"use client";23import {4 motion,5 useAnimationFrame,6 useMotionValue,7 useReducedMotion,8} from "motion/react";9import {10 useCallback,11 useEffect,12 useRef,13 useState,14 type ReactNode,15} from "react";1617interface MarqueeProps {18 children: ReactNode;19 /** Pixels travelled per second. */20 speed?: number;21 direction?: "left" | "right";22 pauseOnHover?: boolean;23 /** Fade the leading and trailing edges into the background. */24 fadeEdges?: boolean;25 className?: string;26}2728/**29 * A seamless, infinitely scrolling row.30 *31 * Driven by `useAnimationFrame` against a motion value rather than a CSS32 * keyframe. That costs nothing extra and buys two things a keyframe cannot:33 * pausing mid-travel without a jump, and a wrap point measured from the real34 * rendered width, so any content loops seamlessly without hand-tuned timings.35 */36export function Marquee({37 children,38 speed = 60,39 direction = "left",40 pauseOnHover = true,41 fadeEdges = true,42 className = "",43}: MarqueeProps) {44 const trackRef = useRef<HTMLDivElement>(null);45 const x = useMotionValue(0);46 const [paused, setPaused] = useState(false);47 const [halfWidth, setHalfWidth] = useState(0);48 const prefersReducedMotion = useReducedMotion();4950 const measure = useCallback(() => {51 const track = trackRef.current;52 if (!track) return;53 // The content is rendered twice, so one loop is half the track.54 setHalfWidth(track.scrollWidth / 2);55 }, []);5657 useEffect(() => {58 measure();59 const track = trackRef.current;60 if (!track) return;6162 const observer = new ResizeObserver(measure);63 observer.observe(track);64 return () => observer.disconnect();65 }, [measure]);6667 useAnimationFrame((_, delta) => {68 if (paused || halfWidth === 0 || prefersReducedMotion) return;6970 // delta is milliseconds since the last frame, so movement stays constant71 // regardless of refresh rate.72 const step = (speed * delta) / 1000;73 let next = x.get() + (direction === "left" ? -step : step);7475 // Wrap by exactly one copy's width; the seam is never visible.76 if (next <= -halfWidth) next += halfWidth;77 if (next >= 0) next -= halfWidth;7879 x.set(next);80 });8182 return (83 <div84 className={`relative w-full overflow-hidden ${className}`}85 style={86 fadeEdges87 ? {88 maskImage:89 "linear-gradient(to right, transparent, black 12%, black 88%, transparent)",90 WebkitMaskImage:91// … truncated
What it pulls in
npm packages
Files it writes
More from ui-beats
All 34 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Animated Beamanimated-beam | ui-beats | Components | Free | 1 dep | |
| Animated Listanimated-list | ui-beats | Components | Free | 1 dep | |
| Border Beamborder-beam | ui-beats | Components | Free | 1 dep | |
| Bouncebounce | ui-beats | Components | Free | 1 dep | |
| Card Stackcard-stack | ui-beats | Components | Free | 1 dep | |
| Dockdock | ui-beats | Components | Free | 1 dep | |
| Fade Infade-in | ui-beats | Components | Free | 1 dep | |
| Fade In Unblurfade-in-unblur | ui-beats | Components | Free | 1 dep |
