Text split

Splits text into lines, words or characters and reveals them in sequence as the text scrolls into view.

<tp-mo-text-split>
Text split
Default density Compact density Restart from the initial state Show the copyable source

Quiet spaces, open light, and rooms that breathe

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.

Words behind a mask
Default density Compact density Restart from the initial state Show the copyable source

Ship the release on Friday

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.

Characters from the center
Default density Compact density Restart from the initial state Show the copyable source

Open light

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.

Slow stagger
Default density Compact density Restart from the initial state Show the copyable source

Every time it enters

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.

Reveal on demand
Default density Compact density Restart from the initial state Show the copyable source

Your workspace is ready

Reveal

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.

Custom reveal
Default density Compact density Restart from the initial state Show the copyable source

Launch week starts Monday

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.

Inline formatting
Default density Compact density Restart from the initial state Show the copyable source

Read the migration guide before upgrading, and back up your workspace first.

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.

Scrubbed by the scroll
Default density Compact density Restart from the initial state Show the copyable source

Quiet spaces reveal as you scroll

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 split with chars switches 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-diagnostic warning 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; lines lists them only when split includes lines.
  • 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-line and ::first-letter do not apply to split text.
  • text-align: justify stretches lines as usual, but every recalculation measures again.
  • hyphens: auto can 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-split is treated as one piece of the outer one.

API Reference

Every element also accepts motion-policy.

Attributes

AttributePropertyTypeDefaultDescriptionsplitsplitstring'words'Units to create: tokens of chars, words, lines. The finest unit animates.maskmaskMoTextSplitMask'none'The unit whose pieces are clipped, so they emerge from behind it.revealrevealstring | undefinedundefinedEffect tokens (fade, up, down, left, right, zoom-in, zoom-out, blur). Unset uses the coordinator default, or fade up; empty disables the reveal.staggerstaggernumber30Milliseconds between consecutive pieces.stagger-fromstaggerFromStaggerFrom'first'The piece the stagger starts from.reveal-repeatrevealRepeatbooleanfalseReveal again on every viewport entry instead of only the first.reveal-holdrevealHoldbooleanfalseWhile set, a ready reveal waits in its start state. Setting it on a revealed text hides it, playing the reveal backwards; removing it reveals the text again.

Properties

PropertyTypeDefaultDescriptionlinesread-onlyHTMLElement[]—The line pieces, in order (empty unless lines are split).wordsread-onlyHTMLElement[]—The word pieces, in order.charsread-onlyHTMLElement[]—The character pieces, in order (empty unless characters are split).masksread-onlyHTMLElement[]—The mask wrappers, in order.revealedread-onlyboolean—Whether the reveal has played (false again after a repeat reset).

Events

EventTypeDescriptiontp-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

MethodReturnsDescriptionsplitText()voidSplits again from the original content (after revert(), or to force a recalculation).revert()voidRestores the original content and stops splitting until splitText() is called.

Slots

SlotDescriptiondefaultThe text, with inline formatting and interactive elements.

CSS custom properties

PropertyDefaultDescription--tp-mo-text-split-duration—Per-piece reveal duration. Default var(--tp-duration-slow).--tp-mo-text-split-easing—Reveal easing. Default var(--tp-easing-enter).--tp-mo-text-split-distance—Travel without a mask. Default 0.4em.--tp-mo-text-split-scale—Scale offset for zoom-in and zoom-out. Default 0.2.--tp-mo-text-split-blur—Start blur for blur. Default 0.15em.--tp-mo-text-split-mask-bleed—How far masks extend over ascenders and descenders. Default 0.1em.