Skip to content
ARDENT / MOTION LAB
Library

Typography · Starter

Split Text Reveal

Text is split into words (overflow-hidden masks) and characters. Each character animates y and a slight rotation with an expo-out curve.

Live preview

Plays on load. Use Replay above or the button inside.

Copy-ready · type-checkedThe preview above renders this exact file. Reduced-motion fallback included.
/**
 * Split Text Reveal
 * Masked characters resolve into a quiet editorial headline.
 * Requires: motion, Tailwind with shadcn semantic tokens.
 */
import { useState } from "react";
import { motion, useReducedMotion } from "motion/react";

export default function SplitTextReveal({
  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 [replay, setReplay] = useState(0);
  const text = "The art of paying attention.";

  return (
    <section className="flex min-h-[80vh] items-center justify-center bg-background px-6 py-20 text-foreground">
      <div className="w-full max-w-5xl text-center">
        <p className="mb-10 font-mono text-xs uppercase tracking-[0.2em] text-muted-foreground">
          Ardent / Studies in motion / 01
        </p>
        <h2
          key={replay}
          aria-label={text}
          className="font-serif text-5xl leading-[1.08] tracking-tight sm:text-7xl lg:text-8xl"
        >
          {text.split(" ").map((word, wi) => (
            <span
              key={wi}
              aria-hidden="true"
              className="mr-[0.22em] inline-block overflow-hidden pb-[0.15em] align-bottom"
            >
              {Array.from(word).map((letter, ci) => (
                <motion.span
                  key={ci}
                  className="inline-block"
                  initial={
                    reduced ? false : { y: `${110 * amount}%`, rotate: 6 * amount, opacity: 0 }
                  }
                  animate={{ y: 0, rotate: 0, opacity: 1 }}
                  transition={{
                    duration: reduced ? 0 : 0.9,
                    delay: reduced ? 0 : wi * 0.09 + ci * 0.018,
                    ease: [0.16, 1, 0.3, 1],
                  }}
                >
                  {letter}
                </motion.span>
              ))}
            </span>
          ))}
        </h2>
        <p className="mx-auto mt-8 max-w-sm text-sm leading-relaxed text-muted-foreground">
          Less noise. More meaning. A small study in how words arrive.
        </p>
        <button
          type="button"
          onClick={() => setReplay((value) => value + 1)}
          className="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"
        >
          Replay reveal
        </button>
      </div>
    </section>
  );
}
Next patternStaggered Line Typography→