Typography · Starter
Editorial Marquee
Content is duplicated and translated -50% on a linear CSS keyframe for a seamless loop. Rows run in opposite directions.
Live preview
Hover a row to pause, or use the Pause motion button.
Copy-ready · type-checkedThe preview above renders this exact file. Reduced-motion fallback included.
/**
* Editorial Marquee
* Counter-running service lists with explicit pause controls.
* Requires: motion, Tailwind with shadcn semantic tokens.
*/
import { useState } from "react";
import { useReducedMotion } from "motion/react";
export default function Marquee({
reduced: reducedProp,
intensity = 1,
}: {
reduced?: boolean;
intensity?: number;
}) {
const systemReduced = useReducedMotion();
const reduced = reducedProp ?? systemReduced ?? false;
const amount = Math.min(1.5, Math.max(0.5, intensity));
const [paused, setPaused] = useState(false);
const words = ["Brand systems", "Art direction", "Digital craft", "Moving image"];
return (
<section className="flex min-h-[80vh] flex-col justify-center overflow-hidden bg-background py-20 text-foreground">
<style>{`
@keyframes ardent-marquee { to { transform: translateX(-50%); } }
.ardent-marquee-row:hover .ardent-marquee-track,
.ardent-marquee-row:focus-within .ardent-marquee-track { animation-play-state: paused !important; }
@media (prefers-reduced-motion: reduce) { .ardent-marquee-track { animation: none !important; } }
`}</style>
<p className="mb-10 px-6 text-center font-mono text-xs uppercase tracking-[0.2em] text-muted-foreground">
An independent practice / 06
</p>
{reduced ? (
<ul className="mx-auto flex max-w-5xl flex-wrap justify-center gap-x-10 gap-y-5 px-6">
{words.map((word) => (
<li key={word} className="font-serif text-4xl tracking-tight sm:text-6xl">
{word}
</li>
))}
</ul>
) : (
[false, true].map((reverse) => (
<div
key={String(reverse)}
className="ardent-marquee-row overflow-hidden border-y border-border py-6"
>
<div
className="ardent-marquee-track flex w-max"
style={{
animation: `ardent-marquee ${38 / amount}s linear infinite`,
animationDirection: reverse ? "reverse" : "normal",
animationPlayState: paused ? "paused" : "running",
}}
>
{[0, 1].map((copy) => (
<ul key={copy} aria-hidden={copy === 1} className="flex shrink-0 items-center">
{words.map((word) => (
<li
key={word}
className="flex shrink-0 items-center gap-10 pr-10 font-serif text-5xl tracking-tight sm:text-7xl"
>
{word}
<span aria-hidden="true" className="h-2 w-2 rounded-full bg-accent" />
</li>
))}
</ul>
))}
</div>
</div>
))
)}
{!reduced && (
<button
type="button"
aria-pressed={paused}
onClick={() => setPaused((value) => !value)}
className="mx-auto mt-10 rounded-full border border-border px-6 py-3 text-sm focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-4"
>
{paused ? "Resume motion" : "Pause motion"}
</button>
)}
</section>
);
}
Next patternPinned Horizontal Gallery→