← Back to gallery
CSS

Split Character Stagger

A grapheme-aware split text reveal that uses Intl.Segmenter when available, transform-only staggered spans, and readable fallback text.

intl-segmentergraphemestaggersplit-textaccessibility

Grapheme-aware split · staggered reveal

Split Character Stagger

Grapheme-aware spans stagger into place when Intl.Segmenter is available, while the final readable phrase stays the source of truth. Three variants demonstrate Latin headline, emoji-safe segmentation, and a Hangul phrase whose visual split follows user-perceived characters.

Letter rise · short cascade

Latin Headline

A concise word reveals one grapheme at a time with transform + opacity only. Visual split is independent from layout measurement, avoiding hard-coded text widths.

  • grapheme
  • stagger
  • transform

Combined glyphs stay intact

Emoji-Safe Label

The segmentation path avoids slicing emoji or combining sequences into broken pieces. Uses Intl.Segmenter when available, falling back to Array.from for older browsers with simple strings.

  • Intl.Segmenter
  • emoji-safe
  • fallback

Non-Latin split guard

Hangul Phrase

A non-Latin phrase demonstrates that the visual split should follow user-perceived characters. The Hangul syllable blocks stay intact — never split into jamo components.

  • non-latin
  • cluster-safe
  • readability

Split inspector

Latin Headline

  • grapheme
  • stagger
  • transform

A concise word reveals one grapheme at a time with transform + opacity only. Visual split is independent from layout measurement, avoiding hard-coded text widths.

// JSX — segment with Intl.Segmenter, fall back to Array.from
const graphemes = (() => {
  if (typeof Intl?.Segmenter !== 'function') return Array.from(phrase);
  const seg = new Intl.Segmenter(undefined, { granularity: 'grapheme' });
  return Array.from(seg.segment(phrase), (s) => s.segment);
})();

return (
  <p className="split-phrase" aria-label={phrase}>
    {graphemes.map((g, i) => (
      <span key={i} className="split-grapheme" style={{ '--i': i }}>
        {g === ' ' ? '\u00a0' : g}
      </span>
    ))}
  </p>
);

/* CSS */
.split-phrase {
  color: #f5f3ff;
}

.split-grapheme {
  display: inline-block;
  transform-origin: 50% 100%;
  animation: split-rise 2.60s cubic-bezier(0.22, 1.4, 0.36, 1)
    infinite both;
  animation-delay: calc(var(--i, 0) * 70ms);
}

@keyframes split-rise {
  0%   { opacity: 0; transform: translateY(14px) scale(0.94); }
  35%  { opacity: 1; transform: translateY(-3px) scale(1.04); }
  55%  { opacity: 1; transform: translateY(0) scale(1); }
  85%  { opacity: 1; transform: translateY(0); }
  100% { opacity: 0; transform: translateY(-4px); }
}

@media (prefers-reduced-motion: reduce) {
  .split-grapheme { animation: none; opacity: 1; transform: none; }
}