Skip to content
ARDENT / MOTION LAB
Library

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→