AnimatedCSS
Marquee
Infinite ticker with faded edges that pauses on hover. Works as a Server Component.
ReactTypeScriptTailwind CSSMotionNext.jsViteshadcn
ReactTypeScriptTailwind CSSMotionNext.jsViteshadcn
Installation
npx shadcn@latest add https://ui-collections.vercel.app/r/tide/marquee.jsonAlso installs the theme, cn(). Packages: clsx, tailwind-merge.
Usage
marquee-demo.tsx
import { Marquee } from "@/registry/kit/ui/marquee";
const stack = ["React", "TypeScript", "Tailwind CSS", "Motion", "Next.js", "Vite", "shadcn"];
export default function MarqueeDemo() {
return (
<div className="grid w-full gap-6">
<Marquee duration={36}>
{stack.map((name) => (
<span key={name} className="flex items-center gap-12 font-display text-4xl tracking-[-0.02em] whitespace-nowrap">
{name}
<span aria-hidden className="text-base text-accent">
✦
</span>
</span>
))}
</Marquee>
<Marquee duration={28} reverse gap="1rem">
{stack.map((name) => (
<span key={name} className="surface rounded-control px-5 py-2 text-sm whitespace-nowrap text-fg/85">
{name}
</span>
))}
</Marquee>
</div>
);
}API
duration= 30
numberSeconds per loop.
gap= "3rem"
stringSpace between items.
reverse= false
booleanScroll right instead of left.
pauseOnHover= true
booleanStop while hovered.
fade= true
booleanFade the edges.
Source
components/ui/marquee.tsx
import type { CSSProperties, HTMLAttributes } from "react";
import { cn } from "@/registry/kit/lib/utils";
export interface MarqueeProps extends HTMLAttributes<HTMLDivElement> {
/** Seconds for one full loop. */
duration?: number;
/** Any CSS length between items. */
gap?: string;
reverse?: boolean;
pauseOnHover?: boolean;
/** Fade the left and right edges. */
fade?: boolean;
}
/**
* Infinite horizontal ticker. CSS-only (works as a Server Component); the
* duplicate track is `inert` so links aren't announced or focused twice.
*/
export function Marquee({
duration = 30,
gap = "3rem",
reverse = false,
pauseOnHover = true,
fade = true,
className,
style,
children,
...props
}: MarqueeProps) {
const track = "flex shrink-0 items-center gap-(--marquee-gap) pr-(--marquee-gap)";
return (
<div
className={cn(
"group/marquee flex overflow-hidden",
fade && "mask-[linear-gradient(to_right,transparent,#000_12%,#000_88%,transparent)]",
className,
)}
style={{ "--marquee-duration": `${duration}s`, "--marquee-gap": gap, ...style } as CSSProperties}
{...props}
>
<div
className={cn(
"flex w-max shrink-0 animate-marquee",
reverse && "[animation-direction:reverse]",
pauseOnHover && "group-hover/marquee:[animation-play-state:paused]",
)}
// Set here: the shared animation shorthand resolves its duration on :root.
style={{ animationDuration: `${duration}s` }}
>
<div className={track}>{children}</div>
<div className={track} aria-hidden inert>
{children}
</div>
</div>
</div>
);
}