Skip to content

TextSplit

Reveal text by character, word or line while keeping one readable copy for assistive technology.

Use it on display type that earns an entrance: a launch headline, a hero, a section opener. Keep it off body copy, labels and anything people need to read the moment the page opens. For a block of content use Reveal, and for a group of elements use Stagger.

Installation

npm i @youj/motion

import { TextSplit } from '@youj/motion'

For an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.

Usage

Pass the text as a string and choose what to split it by. By default it splits by word and rises into place the first time 40% of it is on screen.

import { TextSplit } from '@youj/motion'

export function Headline() {
  return (
    <TextSplit as="h1" by="lines" mask className="text-5xl font-semibold tracking-tight">
      Every branch gets a preview you can share in seconds.
    </TextSplit>
  )
}

as sets the element, so a heading stays a heading. children must be a plain string: the split needs the characters themselves.

Examples

Split modes

by chooses the unit. Each mode has its own default step, because a character is small and a line is large: chars uses tight, words uses base and lines uses loose. Any Reveal variant works for the units, and step overrides the default.

In chars mode, each word stays in one unbreakable span, so a word never wraps in the middle.

Reference

Propschildren, as, by… (13)

Props

TextSplit props
PropType
childrenRequiredstring

The text. Rendered once for assistive tech; the split copy is aria-hidden.

–
asElementType

default 'span'

'span'
bySplitBy
'chars''words''lines'

default 'words'

'words'
variantRevealVariant
'fade''rise''blur-in''scale''wipe-up''wipe-down''wipe-left''wipe-right'

default 'rise'

'rise'
maskboolean

Clip each unit to its own box, so 'rise' slides up from behind a line.

default false

false
stepStaggerToken
'tight''base''loose'
–
delaynumber

Initial delay, in steps.

default 0

0
inViewInView

When it plays: false on mount; true (the default) when scrolled into view, once, at 40% visible; or { once, amount, margin }.

–
durationDurationToken
'instant''fast''base''slow''deliberate'
–
easeEasingToken
'standard''enter''exit''emphasized''linear'
–
distancenumber | string–
Other props pass through: extends HTMLAttributes<HTMLElement>.
MotionEach unit, step, chars, step, words… · reduced motion

Motion

Motion tokens
MomentTokens
Each unitthe default rise; mask travels 105% instead of 12px
duration.slowease.enter

340ms · cubic-bezier(0.16, 1, 0.3, 1)

340mscubic-bezier(0.16, 1, 0.3, 1)
Step, chars
stagger.tight

24ms step

24ms step
Step, words
stagger.base

40ms step

40ms step
Step, lines
stagger.loose

72ms step

72ms step

Reduced motion

Renders the plain string in its final state, unsplit.

The units animate only opacity, transform, filter and clip-path, so a split never triggers layout while it plays.

Reduced motion

TextSplit renders the string as plain text in its element, with no split and no hidden copy, and skips line measurement entirely. The headline reads exactly as written, at once.

AccessibilityARIA, announcements

Accessibility

  • The full string is rendered once in a visually hidden span, and the split copy is aria-hidden. A screen reader reads the headline as a sentence, never letter by letter.
  • With as="h1" or any other heading, the heading's accessible name is the whole string.
  • Each unit is an element. Keep chars to short strings, and prefer words or lines for anything longer than a headline.