React · Spring Physics · TypeScript

TextEngine

Spring-animated text for React. Split any string into lines, words or letters — each independently driven by physics-based springs.

6modes
50+props
3layers
1extra dep

Examples

See it in action

01

Line by line

Each line reveals independently. Ideal for headlines and multi-line copy — pair with overflow to mask letters as they slide.

mode="once"
<TextEngine
mode="once"
overflow
lineIn={{ opacity: 1, y: 0 }}
lineOut={{ opacity: 0, y: 60 }}
lineStagger={80}
lineConfig={{
duration: 1000,
easing: easings.easeOutQuart,
}}
>
Motion is the language
of human intent.
</TextEngine>
Motion is the language
of human intent.
02

Word cascade

Words animate as independent units with staggered delay. Spring tension and friction give it a natural, organic feel.

mode="always"
<TextEngine
mode="always"
wordIn={{ opacity: 1, y: 0 }}
wordOut={{ opacity: 0, y: 20 }}
wordStagger={55}
wordConfig={{ tension: 160, friction: 18 }}
>
Every word finds its moment.
</TextEngine>
Every word finds its moment.
03

Letter by letter

Per-character stagger with scale and opacity. Tight stagger feels like a typewriter; loose stagger feels cinematic.

mode="always"
<TextEngine
mode="always"
letterIn={{ opacity: 1, y: 0, scale: 1 }}
letterOut={{ opacity: 0, y: 30, scale: 0.7 }}
letterStagger={28}
letterConfig={{ tension: 220, friction: 22 }}
>
Character by character.
</TextEngine>
Character by character.
04

Mixed inline content

Inline elements like <span> are treated as word wrappers — their styles, classes and props are preserved while each word inside animates normally.

mode="always"
<TextEngine
mode="always"
overflow
wordIn={{ opacity: 1, y: 0 }}
wordOut={{ opacity: 0, y: 24 }}
wordStagger={60}
wordConfig={{
duration: 700,
easing: easings.easeOutCubic,
}}
>
Styled{' '}
<span style={{ fontWeight: 900, color: '#7c6fef' }}>
inline
</span>
{' '}elements animate too.
</TextEngine>
Styled inline elements animate too.
05

Overflow clip

Wrap each letter in an overflow:hidden container so characters emerge from — and retreat into — a masked boundary, giving a refined editorial reveal.

mode="always"
<TextEngine
mode="always"
overflow
wrapLetterIn={{ overflow: 'hidden' }}
letterIn={{ opacity: 1, y: '0%' }}
letterOut={{ opacity: 0, y: '100%' }}
letterStagger={35}
letterConfig={{ tension: 180, friction: 20 }}
>
Clipped inside each letter.
</TextEngine>
Clipped inside each letter.
06wild

Elastic spring bounce

Crank up the tension to 500 and drop friction to 6 — each letter overshoots and bounces back. Pure spring physics, no keyframes.

mode="always"
<TextEngine
mode="always"
letterIn={{ opacity: 1, y: 0, scale: 1 }}
letterOut={{ opacity: 0, y: -80, scale: 2.2 }}
letterStagger={40}
letterConfig={{ tension: 500, friction: 6 }}
>
Elastic spring physics.
</TextEngine>
Elastic spring physics.
07wild

3D perspective flip

Rotate each letter on the X axis while clipping it — creates a slot-machine flip effect. Combine with word stagger for a cascade.

mode="always"
<TextEngine
mode="always"
overflow
wrapLetterIn={{ overflow: 'hidden' }}
letterIn={{ opacity: 1, rotateX: 0 }}
letterOut={{ opacity: 0, rotateX: -90 }}
letterStagger={50}
letterConfig={{ tension: 200, friction: 18 }}
>
3D flip. Per letter.
</TextEngine>
3D flip. Per letter.
08wild

Everything. All at once.

Stack y, x, rotate and scale simultaneously on every letter. Each spring resolves independently — the result is beautifully chaotic.

mode="always"
<TextEngine
mode="always"
letterIn={{
opacity: 1, y: 0, x: 0,
rotate: 0, scale: 1,
}}
letterOut={{
opacity: 0, y: 60, x: -20,
rotate: -25, scale: 0.4,
}}
letterStagger={45}
letterConfig={{ tension: 260, friction: 14 }}
>
Everything. All at once.
</TextEngine>
Everything. All at once.
09

Scroll-driven words

Tie animation progress to the scroll position. Words reveal as the element travels through the viewport — no threshold snapping.

mode="progress"
<TextEngine
mode="progress"
wordIn={{ opacity: 1, y: 0 }}
wordOut={{ opacity: 0, y: 20 }}
wordConfig={{ duration: 400 }}
start="top bottom"
end="bottom center"
type="interpolate"
interpolationStaggerCoefficient={0.3}
>
Driven by scroll position.
</TextEngine>
Driven by scroll position.
10

Scroll toggle snap

type="toggle" fires a one-shot play-in when the element crosses the start threshold and play-out when it crosses end — no continuous tracking needed.

mode="progress"
<TextEngine
mode="progress"
wordIn={{ opacity: 1, y: 0, scale: 1 }}
wordOut={{ opacity: 0, y: 20, scale: 0.9 }}
wordStagger={60}
wordConfig={{ tension: 180, friction: 20 }}
start="top bottom"
end="top center"
type="toggle"
>
Snaps in as you scroll past.
</TextEngine>
Snaps in as you scroll past.
11wild

Scroll blur interpolation

Combine filter: blur with opacity and y in progress mode. Each letter un-blurs as it scrolls into the focal zone — scroll back and watch it dissolve again.

mode="progress"
<TextEngine
mode="progress"
letterIn={{
opacity: 1, y: 0,
filter: 'blur(0px)',
}}
letterOut={{
opacity: 0, y: 40,
filter: 'blur(16px)',
}}
letterStagger={25}
letterConfig={{ duration: 600 }}
start="top bottom"
end="center center"
type="interpolate"
interpolationStaggerCoefficient={0.25}
>
Blur fades with your scroll.
</TextEngine>
Blur fades with your scroll.
12

Head-on collision

Word wrappers fall from above while letters rise from below — they collide into place. wrapWordConfig drives the outer container; letterConfig drives the inner characters independently.

mode="always"
<TextEngine
mode="always"
wrapWordIn={{ y: 0, opacity: 1 }}
wrapWordOut={{ y: -50, opacity: 0 }}
wordConfig={{ tension: 160, friction: 16 }}
letterIn={{ opacity: 1, y: 0 }}
letterOut={{ opacity: 0, y: 24 }}
letterStagger={14}
letterConfig={{ tension: 280, friction: 22 }}
wordStagger={80}
>
They fall. Rise. Collide.
</TextEngine>
They fall. Rise. Collide.
13wild

Gear mechanism

Each letter's wrapper spins in 180° while the letter itself counter-rotates −90°. Two transforms working against each other — the result looks mechanical, like interlocking gears.

mode="always"
<TextEngine
mode="always"
wrapLetterIn={{ rotate: 0, scale: 1 }}
wrapLetterOut={{ rotate: 180, scale: 0 }}
letterIn={{ opacity: 1, rotate: 0 }}
letterOut={{ opacity: 0, rotate: -90 }}
letterStagger={28}
letterConfig={{ tension: 320, friction: 12 }}
>
Gears within gears.
</TextEngine>
Gears within gears.
14

Breathing lines

Line wrappers expand outward from a tiny scale (0.15→1) while the text inside simultaneously rises. The combination reads as the lines breathing open rather than simply sliding in.

mode="always"
<TextEngine
mode="always"
wrapLineIn={{ scale: 1, opacity: 1 }}
wrapLineOut={{ scale: 0.15, opacity: 0 }}
lineIn={{ y: 0, opacity: 1 }}
lineOut={{ y: 18, opacity: 0 }}
lineStagger={130}
lineConfig={{ tension: 140, friction: 18 }}
>
Lines breathe
into the frame.
</TextEngine>
Lines breathe
into the frame.
15wild

Parallelogram shear

Each word wrapper skews −20° while the word itself counter-skews +20°. The opposing transforms cancel out at rest — during the animation the words appear to shear in from a diagonal plane.

mode="always"
<TextEngine
mode="always"
wrapWordIn={{ skewX: 0 }}
wrapWordOut={{ skewX: -20 }}
wordIn={{ opacity: 1, skewX: 0 }}
wordOut={{ opacity: 0, skewX: 20 }}
wordStagger={65}
wordConfig={{ tension: 200, friction: 16 }}
>
Sliced. Sheared. Served.
</TextEngine>
Sliced. Sheared. Served.
16wild

Three-layer storm

Three independent animation layers stacked: word wrappers slide in from the left, letter wrappers drop from above with a spring bounce, and each letter spins 270° into place. All three run simultaneously with separate stagger clocks.

mode="always"
<TextEngine
mode="always"
wrapWordIn={{ x: 0, opacity: 1 }}
wrapWordOut={{ x: -50, opacity: 0 }}
wrapLetterIn={{ y: 0, scale: 1 }}
wrapLetterOut={{ y: -32, scale: 0 }}
letterIn={{ opacity: 1, rotate: 0 }}
letterOut={{ opacity: 0, rotate: 270 }}
wordStagger={90}
letterStagger={18}
wordConfig={{ tension: 150, friction: 16 }}
letterConfig={{ tension: 380, friction: 10 }}
>
Three layers. Pure chaos.
</TextEngine>
Three layers. Pure chaos.
17

Venetian rise

Each letter's wrapper descends from above while the letter rises from below. overflow clips both — they're invisible until they converge, then snap into place like venetian blinds opening.

overflow + wrapLetter
<TextEngine
mode="always"
overflow
wrapLetterIn={{ y: 0 }}
wrapLetterOut={{ y: -52 }}
letterIn={{ y: 0, opacity: 1 }}
letterOut={{ y: 52, opacity: 0 }}
letterStagger={20}
letterConfig={{ tension: 240, friction: 22 }}
>
Blinds open. Letters rise.
</TextEngine>
Blinds open. Letters rise.
18

Door slide

The word wrapper slides in from the right, the word itself from the left. With overflow clipping both, each word is invisible until the two halves slide together — like a pair of doors closing.

overflow + wrapWord
<TextEngine
mode="always"
overflow
wrapWordIn={{ x: 0 }}
wrapWordOut={{ x: 70 }}
wordIn={{ x: 0, opacity: 1 }}
wordOut={{ x: -70, opacity: 0 }}
wordStagger={55}
wordConfig={{ tension: 180, friction: 18 }}
>
Doors slide open.
</TextEngine>
Doors slide open.
19

Stage curtain

Line wrappers drop from below while the lines themselves rise from above. overflow keeps each invisible until they meet — the stage curtain drops and the line is revealed behind it.

overflow + wrapLine
<TextEngine
mode="always"
overflow
wrapLineIn={{ y: 0 }}
wrapLineOut={{ y: 110 }}
lineIn={{ y: 0, opacity: 1 }}
lineOut={{ y: -110, opacity: 0 }}
lineStagger={120}
lineConfig={{ tension: 140, friction: 16 }}
>
Each line drops its
curtain.
</TextEngine>
Each line drops
its curtain.
20wild

Flip card

The wrapper tilts backward (−22° skewY) while the letter tilts forward (+22°). overflow makes the letter invisible until both skews resolve — each character appears to hinge open like a card flipping face-up.

overflow + wrapLetter skewY
<TextEngine
mode="always"
overflow
wrapLetterIn={{ skewY: 0 }}
wrapLetterOut={{ skewY: -22 }}
letterIn={{ opacity: 1, skewY: 0 }}
letterOut={{ opacity: 0, skewY: 22 }}
letterStagger={22}
letterConfig={{ tension: 280, friction: 18 }}
>
Cards flip open.
</TextEngine>
Cards flip open.
21

Lift gates

Word wrappers rise from below while words themselves fall from above.overflow hides each until they align — like stadium gates lifting to reveal the words waiting above them.

overflow + wrapWord
<TextEngine
mode="always"
overflow
wrapWordIn={{ y: 0 }}
wrapWordOut={{ y: 56 }}
wordIn={{ y: 0, opacity: 1 }}
wordOut={{ y: -56, opacity: 0 }}
wordStagger={65}
wordConfig={{ tension: 200, friction: 16 }}
>
Gates lift. Words land.
</TextEngine>
Gates lift. Words land.