@magic-spells/split-text

Words
that rise.

A tiny web component that splits text and animates it into view, by word, character, or line.

Install npm i @magic-spells/split-text
JS (gzip) 2.6 KB
CSS (gzip) 0.8 KB
Dependencies 0
// install npm i @magic-spells/split-text // import (auto-registers the <split-text> element) import '@magic-spells/split-text' // or via CDN <script src="https://unpkg.com/@magic-spells/split-text"></script> <link rel="stylesheet" href="https://unpkg.com/@magic-spells/split-text/css/min"> // use it <split-text>The fox quietly steps across the morning grass.</split-text>

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.

The fox quietly steps across the cool morning grass.

<split-text>The fox quietly steps across the cool morning grass.</split-text>

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.

Type is just a field of points waiting to settle. Give each letter its own trajectory and the whole paragraph assembles itself — pieces drifting in from every direction, snapping into place one grapheme at a time until the words resolve and the line finally holds still. Emoji and accented characters ride along too: café · résumé · 🌒 · naïve.

<split-text effect="magnetic" split="chars" stagger="12">…</split-text>

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.

There is a kind of motion that doesn't draw attention to itself. It just lets the page settle into place, the way a curtain falls or a page turns. That's the idea here. A handful of bytes, a small piece of CSS, and the words arrive exactly when you scroll to them.

<split-text split="lines" stagger="120">…</split-text>

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.

Built with care by someone who reads the source.
It plays nicely with real HTMLlinks 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.

Words that land where they belong.

<split-text effect="drop">…</split-text>

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.

Sliding in from the right.

And from the left.

<split-text effect="slide-right" split="chars">…</split-text>
<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.

A gentle focus into view.

Dialed up for drama.

<split-text effect="bloom" split="chars">…</split-text>
/* 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.

Standing up from the baseline.

Swinging open like a door.

Same effect, center pivot, closer camera.

<split-text effect="spin-x" split="chars">…</split-text>
<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".

Snapped into focus.

<split-text effect="magnetic" split="chars">…</split-text>

Tune the timing.

Adjust delay, stagger, and duration with the sliders.

Words tuned to your liking.

Drag the sliders and the same settings ripple across a longer passage. Every word, character, or line picks up your timing in real time.

Delay 0ms
Stagger 40ms
Duration 800ms
Effect
Easing

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.

Calm and even.

A spring overshoot.

Slow start, sharp finish.

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.

Sometimes the smallest motion is the most felt.

<split-text stagger="60" duration="1100" delay="300">…</split-text> // css: --split-text-distance: 50%

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.

Press the button.

// JS document.querySelector('#manual-split').reveal()

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.

Watch the console as you scroll past this line.

el.addEventListener('split-text:complete', (e) => { console.log(e.detail) // { split: 'words', count: 9 } })

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.

Triggered 400px above the bottom edge.

<split-text>…</split-text> // default: offset="20%"
<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, nine custom properties, two methods, two events. That's everything.

Attributes

Attribute
Default
Description
split
words
words · chars · lines
effect
rise
rise · drop · slide-right · slide-left · bloom · spin-x · spin-y · magnetic
delay
0
Initial delay before first unit (ms)
stagger
30
Delay between units (ms)
duration
800
Animation duration per unit (ms)
easing
cubic-bezier(0.16, 1, 0.3, 1)
CSS easing function
trigger
visible
visible · load · manual
offset
20%
Distance above bottom of viewport before firing (e.g. "200px", "20%"; set "0" to disable)

CSS Custom Properties

Property
Default
Description
--split-text-duration
800ms
Animation duration
--split-text-easing
cubic-bezier(0.16, 1, 0.3, 1)
Animation easing
--split-text-distance
100%
Travel distance for rise / drop / slide
--split-text-stagger
30ms
Delay between units
--split-text-delay
0ms
Initial delay
--split-text-perspective
2000px
3D camera distance (spin effects)
--split-text-origin
per-effect
transform-origin for spin (defaults: bottom for spin-x, left for spin-y)
--split-text-blur
2px
Starting blur radius (bloom & magnetic)
--split-text-scale
0.7
Bloom starting scale

Methods & Events

Name
Type
Description
reveal()
method
Trigger animation manually
split()
method
Reset and re-split (after innerHTML changes)
split-text:start
event
Fires when reveal begins · detail { split, count }
split-text:complete
event
Fires after the last unit settles

Accessibility

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.