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→