Text split
Splits text into lines, words or characters and reveals them in sequence as the text scrolls into view.
Usage
Put Text split inside the heading or paragraph whose text it animates and keep typography on that element; the pieces inherit it. Lines are measured from the original text’s own layout, so the split never changes wrapping or height and nothing below moves. A standalone text reveals the first time it enters the viewport.
The stylesheet keeps the text hidden, in its final space, until the element is defined, so the unsplit text never flashes (it appears anyway after three seconds if the script never loads).
Inside a Scroll trigger (or an Image group), that coordinator owns the timing.
Inside a scrubbing Scroll trigger the scroll position drives the same reveal: every piece shows its eased state at the scrubbed time, in stagger order.
Masks
mask clips a unit so pieces rise from behind it. With a mask, up, down, left and right travel the piece’s own size; without one they travel a short distance and fade.
The mask extends a little over ascenders and descenders, so tight line heights never cut a glyph.
Characters and effects
reveal combines effects such as fade, blur, zoom-in and the four directions. split="chars" animates user-perceived characters and turns off kerning from the first frame, so the text never re-wraps when it splits.
Effects only change opacity, translate, scale and filter, so nothing around the text moves. For lines that open from their middle, or glyphs drawn as outlines before they fill, use Text mirror and Text stroke.
Stagger
stagger is the delay in milliseconds between consecutive pieces, and stagger-from starts the sequence at the first, last or center piece. Small values feel like one gesture; large ones read word by word.
reveal-repeat resets the text once it has fully left the viewport and plays again when it returns. All examples here use it so you can scroll back to replay them.
Holding the reveal
reveal-hold keeps a split, visible text waiting in its start state, so you can play it at a moment of your choosing: after a loading screen, when a font arrives, or on a press as here. Removing the attribute plays the reveal if the text is in view. Setting it again on a revealed text hides it, playing the reveal backwards. A reveal that would start during a same-document view transition waits for it to finish.
Custom animation
The reveal runs through the reveal motion role, so you can claim it from a tp-motion-request listener and animate the pieces yourself, with the Web Animations API as here or an animation library. The pieces are on owner.words, owner.chars or owner.lines; return the animation’s finished promise and a cancel so interruptions settle cleanly.
While your driver plays, the built-in transitions are off. If the animation depends on line positions, rebuild it on tp-text-split, which fires whenever the lines change.
Rich text
Formatting is kept around the pieces and links stay the original element, wrapping like ordinary text. Screen readers read the sentence once, with the link and its full name.
Pieces and masks are hidden from assistive technology. The original text of each run is exposed once, in reading order, through visually hidden text, so screen readers, translation and search read the sentence normally.
Under reduced motion every piece appears at rest immediately and the events still fire.
Scroll scrubbing
Scroll the preview down and back up: the lines rise behind their masks with the scroll and sink back as it returns.
Splitting
- Words follow the browser’s word segmentation for the text’s
lang, so Chinese, Japanese and Thai split at word boundaries. Spaces stay plain text between pieces, and words break only where the line could already break (after hyphens and dashes), so punctuation stays with its word. - Characters are user-perceived characters: accents, emoji sequences and flags are never divided. Characters cannot kern or form ligatures, so
splitwithcharsswitches both off from the first frame, before splitting, and the text never re-wraps when it splits. - Scripts whose letters join when shaped (Arabic, the Indic scripts and similar) are never split into characters: such words stay whole and animate as one piece, and a
tp-diagnosticwarning reports it. - Lines are measured from a hidden copy of the original text, so every line-breaking rule the browser applies to the original (alignment,
text-wrap: balance, CJK punctuation) applies to the pieces. Lines are laid out for every split, so words and characters wrap exactly as the original would;lineslists them only whensplitincludeslines. - Formatting such as
<strong>,<em>and<code>is kept around its pieces, cloned once per line when it spans lines, so page styles apply unchanged.<br>and preserved whitespace are kept. - Links wrap like ordinary text. The original
<a>keeps the first fragment, so its listeners, focus and identity stay; fragments on later lines are inert continuations that forward clicks to it. Buttons, form controls, images and other custom elements are kept as the original elements, whole, as one piece.
Inline boxes cannot shape across their edges, so kerning against the neighbouring space is lost and a line can be a fraction of a pixel wider than the original; its breaks and height are still the original’s.
Responsive behaviour
Lines are recalculated in the same frame as the width change, before paint, and only when the change can actually move a word to another line. All texts on the page are measured in one pass. Until then a stale line never wraps, so the page does not jump. A recalculation keeps the reveal state: pending pieces stay hidden, revealed pieces stay at rest, and a reveal in progress completes at once.
Changing the content (for example textContent, or a template library updating the text) is picked up and split again before the next paint. Typography changes that alter the text’s height (font size, line height, a web font loading) recalculate the lines too; after a change that keeps the height, such as letter spacing or weight, call splitText().
Limits
::first-lineand::first-letterdo not apply to split text.text-align: justifystretches lines as usual, but every recalculation measures again.hyphens: autocan break inside a word in the original; such a word is presented whole.- SVG
<text>and vertical writing modes are not split. - A nested
tp-mo-text-splitis treated as one piece of the outer one.
API Reference
Every element also accepts motion-policy.
Attributes
splitsplitstring'words'chars, words, lines. The finest unit animates.maskmaskMoTextSplitMask'none'revealrevealstring | undefinedundefinedfade, up, down, left, right, zoom-in, zoom-out, blur).
Unset uses the coordinator default, or fade up; empty disables the reveal.staggerstaggernumber30stagger-fromstaggerFromStaggerFrom'first'reveal-repeatrevealRepeatbooleanfalsereveal-holdrevealHoldbooleanfalseProperties
linesread-onlyHTMLElement[]wordsread-onlyHTMLElement[]charsread-onlyHTMLElement[]masksread-onlyHTMLElement[]revealedread-onlybooleanEvents
tp-text-splitCustomEvent<{ lines: HTMLElement[]; words: HTMLElement[]; chars: HTMLElement[]; masks: HTMLElement[]; }>{ lines, words, chars, masks } after every split.tp-reveal-changeCustomEvent<{ revealed: boolean; effect: string; }>{ revealed, effect } when the reveal starts or a repeat resets it.tp-reveal-change-completeCustomEvent<{ revealed: boolean; }>{ revealed } after the last piece settles.Methods
splitText()voidrevert(), or to force a recalculation).revert()voidsplitText() is called.Slots
defaultCSS custom properties
--tp-mo-text-split-durationvar(--tp-duration-slow).--tp-mo-text-split-easingvar(--tp-easing-enter).--tp-mo-text-split-distance0.4em.--tp-mo-text-split-scale0.2.--tp-mo-text-split-blurblur. Default 0.15em.--tp-mo-text-split-mask-bleed0.1em.