Use Marquee for non-essential decorative text. It is a default export accepting short strings, not a general-purpose carousel. Keep important information available without movement.
import Marquee from "@/components/ui/marquee";
const features = [
"Source you own",
"Tailwind CSS v4",
"Base UI primitives",
"Light and dark themes",
"Keyboard tested",
"Next.js and Vite",
];
export default function MarqueeDemo() {
return <Marquee items={features} />;
}
#Installation
Complete the base setup. The CLI includes this item's animation styles. For manual installation:
First time? Install the shared base once.
npx shadcn@latest add https://neobrutal-ui.andongmin.com/r/marquee.jsonCreate components/ui/marquee.tsx:
"use client";
import { useState } from "react";
export default function Marquee({ items }: { items: string[] }) {
const [paused, setPaused] = useState(false);
const animationStyle = { animationPlayState: paused ? "paused" : "running" } as const;
return (
<div className="relative flex w-full overflow-x-hidden border-b-2 border-t-2 border-border bg-secondary-background text-foreground font-base">
<button
type="button"
aria-pressed={paused}
aria-label="Pause animation"
onClick={() => setPaused((value) => !value)}
className="absolute right-2 top-2 z-10 rounded-base border-2 border-border bg-secondary-background px-3 py-1 text-sm focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring motion-reduce:hidden"
>
{paused ? "Resume" : "Pause"}
</button>
<div
className="animate-marquee whitespace-nowrap py-12 motion-reduce:flex motion-reduce:w-full motion-reduce:min-w-0 motion-reduce:animate-none motion-reduce:flex-wrap motion-reduce:gap-x-8 motion-reduce:gap-y-3 motion-reduce:whitespace-normal motion-reduce:px-4 motion-reduce:py-6"
style={animationStyle}
>
{items.map((item) => (
<span
key={item}
className="mx-4 text-4xl motion-reduce:mx-0 motion-reduce:max-w-full motion-reduce:[overflow-wrap:anywhere]"
>
{item}
</span>
))}
</div>
<div
aria-hidden="true"
className="absolute top-0 animate-marquee2 whitespace-nowrap py-12 motion-reduce:hidden"
style={animationStyle}
>
{items.map((item) => (
<span key={item} className="mx-4 text-4xl">
{item}
</span>
))}
</div>
</div>
);
}
Also add this item's animation definitions to your Tailwind CSS stylesheet:
@theme inline {
--animate-marquee: marquee 5s linear infinite;
--animate-marquee2: marquee2 5s linear infinite;
@keyframes marquee {
from { transform: translateX(0%); }
to { transform: translateX(-100%); }
}
@keyframes marquee2 {
from { transform: translateX(100%); }
to { transform: translateX(0%); }
}
}#Usage
import Marquee from "@/components/ui/marquee";
export default function FeatureStrip() {
return (
<Marquee
items={[
"Source you own",
"Hard shadows",
"Clear interactions",
"Light and dark",
"Build your way",
"React components",
]}
/>
);
}#API reference
| Prop / behavior | Purpose | Default |
|---|---|---|
items | Short unique text strings | Required |
| Pause / Resume | Stops or resumes both animation strips | Running |
| Reduced motion | Disables decorative movement | User preference respected |
| Duplicate strip | Repeated visual content | Hidden from assistive technology |
#Accessibility
Keep strings unique because they are used as React keys. The native Pause/Resume button is keyboard accessible and stops both strips together. Reduced-motion preferences disable animation and hide the redundant second strip. Do not put essential instructions or controls inside moving content. Supply enough items to cover the available width without gaps.