Each word, on its own.
The default mode. Words slide up sequentially with a 30ms stagger. Drop in a
<split-text> tag and it animates as soon as it scrolls into view —
nothing else to wire up.
Letter by letter.
Use split="chars" for grapheme-level animation. Emoji and accented characters
stay intact — the splitter uses Intl.Segmenter internally so nothing breaks
mid-codepoint. Pair it with effect="magnetic" across a longer passage and
every character flies in from its own scattered start.
Every line, together.
split="lines" measures word positions after layout, groups them by their
rendered top, and animates entire lines together. Inline markup stays in place — no
words are ever re-parented, so links and emphasis survive the split.
Links still link.
Wrap whatever you want — <em>, <strong>,
<a>, <br>. The splitter walks text nodes only, so
inline tags are never moved or unwrapped. Try clicking the link below.
It plays nicely with real HTML —
links remain links,
emphasis stays emphatic, and line breaks are honored as actual line boundaries.
Or fall in from above.
Set effect="drop" to mirror the default rise — words descend into place
instead of rising up. Same timing knobs, same fade.
Slide in from either side.
effect="slide-right" sweeps each unit in from the right;
slide-left mirrors it. Pairs nicely with split="chars" for a
typewriter-meets-marquee feel.
<split-text effect="slide-left" split="chars">…</split-text>
Bloom into focus.
effect="bloom" starts each unit blurred and slightly scaled-down, then
sharpens it in place. No translate — letters appear right where they belong and just
come into focus. Tune the starting state with --split-text-blur and
--split-text-scale.
/* override via CSS custom properties */
style="--split-text-blur: 12px; --split-text-scale: 0.4"
Spin into place.
Two 3D variants. spin-x tilts each unit up from the baseline (rotateX,
origin bottom). spin-y swings each unit open like a door
(rotateY, origin left). The host sets perspective: 2000px by
default — tune via --split-text-perspective, and shift the pivot with
--split-text-origin.
<split-text effect="spin-y" split="chars">…</split-text>
/* override pivot & perspective via CSS */
style="--split-text-origin: center; --split-text-perspective: 600px"
Pulled into place.
effect="magnetic" scatters each character — every piece starts shifted off to
the right by a random distance, at a random height, blurred and transparent. An ease-in
curve makes them drift slowly then snap home, like filings pulled to a magnet. Unlike the
other effects, the pieces are never clipped, so they can travel well outside their box.
Built for split="chars".
Write your own.
An effect is CSS. A resting state, an animation-name bound to
[data-state="revealing"], and a @keyframes rule — that's a new
effect value, with the same delay / stagger /
duration plumbing as every built-in. The ember effect below is
defined in this page's stylesheet; the component ships nothing for it.
SplitText.registerEffect() then adds the two things CSS can't do on its
own — a default easing curve, and a per-unit random start. Here each character gets a
random tilt plus a --split-text-jitter that grows toward the end of the
line, so the tail scatters loose while the opening stays tight.
Tune the timing.
Adjust delay, stagger, and duration with the sliders.
Easing with feeling.
Pass any CSS easing through the easing attribute, or override the
--split-text-easing custom property. Below: three different curves on the
same line.
A subtler rise.
--split-text-distance controls how far each unit travels. Pair a short
travel with a meaningful initial delay for a slow, considered intro.
Drive it yourself.
Set trigger="manual" and call .reveal() from JavaScript when you
want the animation to fire. Useful for orchestrating a sequence with other animations,
or replaying on demand.
Listen in.
Each component fires split-text:start and split-text:complete.
Open your console and scroll past — every reveal logs its mode and unit count.
Choose when it fires.
By default text waits until the element is 20% of the viewport above the
bottom edge — so it doesn't animate while still peeking in from below. Override
offset with a pixel value, a different percentage, or "0" to
disable. The line below waits until it's 400px above the bottom edge before animating.
<split-text offset="400px">…</split-text> // custom pixel value
<split-text offset="0">…</split-text> // disable, fire on first intersection
The whole API, on one screen.
Eight attributes, ten custom properties, two methods, two events, and one static hook for registering effects of your own. That's everything.
Attributes
splitwordswords · chars · linesdelay0stagger30duration800easingcubic-bezier(0.16, 1, 0.3, 1)triggervisiblevisible · load · manualoffset20%"200px", "20%"; set "0" to disable)CSS Custom Properties
--split-text-duration800ms--split-text-easingcubic-bezier(0.16, 1, 0.3, 1)--split-text-distance100%--split-text-stagger30ms--split-text-delay0ms--split-text-perspective2000px--split-text-origintransform-origin for spin (defaults: bottom for spin-x, left for spin-y)--split-text-blur2px--split-text-scale0.7--split-text-jitter0Methods & Events
reveal()split()split-text:start{ split, count }split-text:completeSplitText.registerEffect(name, config){ easing, perUnit(inner, index, total) }, both optionalAccessibility
When the OS preference is reduce motion, the component skips animation
entirely and renders content immediately — no transforms, no blur, no fade. The host's
aria-label is set from the original plain text, so screen readers read the
sentence once instead of one wrapped span at a time. Generated word and character
wrappers are marked aria-hidden, except inside interactive ancestors
(<a>, <button>, etc.) where the accessible name
must be preserved.
Browser support
Modern browsers with custom elements + IntersectionObserver: Chrome 64+,
Firefox 67+, Safari 12.1+, Edge 79+. Intl.Segmenter is used when available
for grapheme-correct character splitting; older Safari falls back to spread iterator.