Overview
Per-character text reveal where each grapheme animates with a staggered animation-delay. The pattern uses Intl.Segmenter (with Array.from fallback) so emoji + non-Latin scripts split on user-perceived character boundaries instead of UTF-16 code units — no broken zero-width joiners, no torn Hangul.
When to use it
Reach for split-char on hero headlines, brand-loud titles, and any string under 25 characters. Skip it for long strings — the cumulative delay becomes a slog. Skip it for any localized string that might contain CJK or bidirectional text without verifying the grapheme split renders correctly.
How it works
At runtime, split the source string with new Intl.Segmenter(locale, { granularity:
'grapheme' }).segment(text) — this yields user-perceived characters that respect emoji zero-width joiners, combining marks, and complex scripts. Wrap each segment in a <span> with an inline animation-delay: calc(var(--i) * 35ms) custom property set per index. Each span shares a single @keyframes rule that animates transform: translateY(0.4em) translateZ(0) and opacity from zero to final. The per-span delay creates the cascade. Wrap the parent in an aria-label with the full string so screen readers announce it as a single phrase.
Production gotchas
Wrapping every character in a span hurts the copy-paste experience — the browser inserts whitespace between span children in some configurations. Set white-space: pre on the wrapper and display: inline-block on each span to preserve spacing without leaking padding. Intl.Segmenter is unsupported in older Safari (< 14.1); detect with typeof Intl.Segmenter === 'function' and fall back to Array.from(text) which handles UTF-16 surrogate pairs but not grapheme clusters. Hover/click interactions on per-character spans can accidentally fire on each glyph — set pointer-events: none on the spans and let the parent handle them.
Accessibility
Set aria-label on the parent so screen readers announce the full phrase rather than reading each span as a separate node, and mark the spans with aria-hidden="true". Under prefers-reduced-motion: reduce set all delays and the keyframe duration to zero so all characters appear simultaneously. Verify the final rendered text passes contrast and is not announced with stutter on JAWS / NVDA after the cascade.
References
Implementation depth
The split should be visual only. Preserve the full phrase as an accessible label, then animate grapheme-safe spans so screen readers do not announce each character as a separate item.
Intl.Segmenter matters for names, emoji, and non-Latin scripts. A string split by code unit can break combined glyphs; if segmentation is not available, prefer word-level stagger over unsafe character splitting.