Text Splitter

Split text into characters, words, or lines and animate them individually with stagger.

Text Splitter breaks a text element into individual <span> nodes — one per character, word, or line — so you can animate each unit independently with stagger.

Basic Usage

Add split to your animation config. The SDK wraps each unit in a <span>, then applies the animation to those spans instead of the parent element.

typescript
import { Motion } from "@motion.page/sdk";

Motion("headline", "h1", {
  split: "chars",
  from: { opacity: 0, y: 20 },
  duration: 0.4,
  stagger: 0.03,
  ease: "power2.out",
}).onPageLoad();

Options

OptionTypeDescription
splitstringSplit type — see values below
maskbooleanWrap each unit in an overflow: hidden container for clip-path-style reveals

split Values

ValueDescription
'chars'Each character becomes a separate animatable span
'words'Each word becomes a separate animatable span
'lines'Each line (based on line wrapping) becomes a separate animatable span
'chars,words'Split into both chars and words — chars are nested inside word spans
'words,lines'Split into both words and lines — words are nested inside line spans
'chars,words,lines'All three levels — chars inside words inside lines

For most text reveal animations, 'chars', 'words', or 'lines' is all you need.

Split Types

Characters

Split into individual characters. Use tight stagger values (0.020.05) so the effect feels snappy.

typescript
Motion("chars-in", "h1", {
  split: "chars",
  from: { opacity: 0, y: 20 },
  duration: 0.4,
  stagger: { each: 0.03, from: "start" },
  ease: "power2.out",
}).onPageLoad();

Words

Split into words. Looser stagger (0.060.1) works well at this scale.

typescript
Motion("words-in", ".tagline", {
  split: "words",
  from: { opacity: 0, x: -15 },
  duration: 0.5,
  stagger: { each: 0.06, from: "start" },
  ease: "power3.out",
}).onPageLoad();

Lines

Split into lines based on the element’s natural line wrapping. Use larger stagger values (0.10.2) since there are typically fewer units.

typescript
Motion("lines-in", "p", {
  split: "lines",
  from: { opacity: 0, y: 40 },
  duration: 0.6,
  stagger: { each: 0.15, from: "start" },
  ease: "power2.out",
}).onPageLoad();

Mask Mode

Set mask: true to wrap each split unit in an overflow: hidden container. This lets you animate y: '110%' to slide text up from behind an invisible clip boundary — a cinematic newspaper-headline effect with no extra CSS required.

typescript
// Words slide up from behind a clip boundary
Motion("headline", "h1", {
  split: "words",
  mask: true,
  from: { y: "110%" },
  duration: 0.6,
  stagger: { each: 0.06, from: "start" },
  ease: "power3.out",
}).onPageLoad();
typescript
// Lines rise from below the clip
Motion("lines-mask", "h2", {
  split: "lines",
  mask: true,
  from: { y: "110%" },
  duration: 0.7,
  stagger: 0.08,
  ease: "power3.out",
}).onPageLoad();

Why 110% instead of 100%? A small extra offset (110%) ensures the starting position clears the clip boundary even with sub-pixel rounding. Use values between 100% and 120%.

The mask containers receive a [data-split-mask] attribute, so you can target them in CSS if needed.

Combining with Stagger

Split text is most powerful with Stagger. Stagger controls how each unit enters in sequence — adjust each, from, and ease to shape the cascade.

typescript
// Classic left-to-right char reveal
Motion("char-reveal", ".hero-title", {
  split: "chars",
  mask: true,
  from: { y: "100%" },
  duration: 0.5,
  stagger: { each: 0.03, from: "start" },
  ease: "expo.out",
}).onPageLoad();

// Center-outward word burst
Motion("word-burst", ".section-title", {
  split: "words",
  from: { opacity: 0, scale: 0.5 },
  duration: 0.5,
  stagger: { each: 0.1, from: "center" },
  ease: "back.out",
}).onPageLoad();

// Random character order — different every play
Motion("char-random", ".scramble", {
  split: "chars",
  from: { opacity: 0, y: 20 },
  duration: 0.4,
  stagger: { each: 0.02, from: "random" },
  ease: "power2.out",
}).onPageLoad();

Scroll-Triggered Text Reveal

Combine split text with scroll trigger to reveal text as it enters the viewport.

typescript
Motion("scroll-title", ".section-title", {
  split: "words",
  mask: true,
  from: { y: "100%", opacity: 0 },
  duration: 0.6,
  stagger: { each: 0.05 },
  ease: "power2.out",
}).onScroll({
  each: true,
  toggleActions: "play none none none",
  start: "top 85%",
});

The each: true option on .onScroll() creates an independent scroll trigger per matched element — useful when the same selector targets multiple section titles.

CSS Targeting

The SDK adds data attributes to every split span. Use them to apply per-character styles in CSS.

typescript
Motion("styled-chars", "h1", {
  split: "chars",
  from: { opacity: 0, rotateY: 90 },
  duration: 0.5,
  stagger: 0.04,
}).onPageLoad();
css
/* Target individual split spans */
[data-split-char]:nth-child(3)  { color: #6633EE; }
[data-split-word]:first-child   { font-weight: bold; }
[data-split-line]               { display: block; }

/* The root element is marked too */
[data-split] { /* styles applied to the container */ }
AttributeApplied to
data-splitRoot element (the target you passed to Motion)
data-split-charEach character span
data-split-wordEach word span
data-split-lineEach line span

Preserved Inline Elements

The splitter only touches bare text nodes. Existing inline elements like <span>, <em>, and <strong> survive splitting intact.

html
<!-- Input -->
<h1>Hello <span class="accent">World</span></h1>

<!-- After split: 'words' — the accent span is preserved -->
<h1 data-split>
  <span data-split-word>Hello</span>
  <span class="accent" data-split-word>World</span>
</h1>

Cleanup and Revert

Killing the timeline reverts the DOM back to the original text content. No leftover span soup.

typescript
// Revert a specific element's split
Motion("headline").kill();

// Revert splits by target (without a named timeline)
Motion.reset(".hero-title");

Common Patterns

Typewriter Effect

Set duration: 0 for an instant snap per character, then use stagger to control the typing speed.

typescript
Motion("typewriter", ".terminal-text", {
  split: "chars",
  from: { opacity: 0 },
  duration: 0,
  stagger: { each: 0.07, from: "start" },
}).onPageLoad();

3D Character Flip

typescript
Motion("char-flip", ".display-title", {
  split: "chars",
  from: { opacity: 0, rotateX: 90 },
  duration: 0.5,
  stagger: { each: 0.03, from: "start" },
  ease: "power3.out",
}).onPageLoad();

Word Blur In

typescript
Motion("word-blur", ".subheading", {
  split: "words",
  from: { opacity: 0, filter: "blur(10px)" },
  duration: 0.6,
  stagger: { each: 0.08, from: "start" },
  ease: "power2.out",
}).onPageLoad();

Lines + Mask on Scroll

typescript
Motion("lines-scroll", ".body-text", {
  split: "lines",
  mask: true,
  from: { y: "110%" },
  duration: 0.7,
  stagger: { each: 0.12, from: "start" },
  ease: "power3.out",
}).onScroll({
  toggleActions: "play none none none",
  start: "top 80%",
});

API Reference

typescript
interface AnimationConfig {
  split?: 'chars' | 'words' | 'lines' | 'chars,words' | 'words,lines' | 'chars,words,lines';
  mask?:  boolean;  // overflow:hidden clip wrapper per unit
}

Related: Stagger · Opacity · Translate