Text Animate
A text animator that works by character, word, or line — typewriter, scramble, wave, and more.
Usage
TextAnimate splits a string into units — characters, words, or lines — and drives every unit
from a single progress value (0 → 1). One small engine therefore covers many effects: it ships
seven presets (typewriter, scramble, rise, highlight, wave, flip, shimmer) and accepts
a custom function for anything else.
It is deliberately headless and font-agnostic: it animates only transform, opacity, and the
displayed glyphs — never font-size, font-weight, line-height, or letter-spacing. So it
inherits whatever typography you set, and multi-line text wraps and breaks naturally.
Two ways to drive it
The component never listens to scroll, viewport, or page load on its own — that keeps it composable. You either let its built-in clock play the animation, or you own the timeline:
When you pass progress, the internal clock is disabled and the component is fully controlled — wire
it to a scroll listener, an IntersectionObserver, a Motion MotionValue, or a slider. When you
omit progress, an opt-in requestAnimationFrame clock plays it once (duration, delay) or
forever (loop).
Examples
Typewriter
The classic effect: characters reveal one-by-one with a blinking caret on the typing edge. Hidden characters take no space, so the line grows as it types.
Scramble
Each character cycles through random glyphs before locking onto its target — the "decode" look. Whitespace is left intact so word boundaries stay stable while the text resolves. Pairs nicely with a monospace font.
Rise
Each unit slides up from behind a clip edge, so the text wipes into view from its own baseline
rather than just translating — a masked reveal that's tedious to wire up by hand. It defaults to a
per-character cascade; switch to by="word" or by="line" for a chunkier reveal.
Reveal on scroll into view
highlight ramps each word from dimmed to full. Here an IntersectionObserver plays the reveal
each time the paragraph scrolls into the panel (and resets it on the way out) — the observer is the
trigger, the component just animates. No animation library required.
Drive it yourself
progress accepts any 0 → 1 value, so you can scrub the animation from a slider, a gesture, or app
state — wire it to a scroll position for a true scrub-as-you-scroll effect, or to any source you like.
Continuous effects
wave and shimmer are continuous: their presets loop the clock automatically, so they animate on
mount with no driver wiring. wave ripples each character vertically (amplitude in em, so it
scales with the font); shimmer sweeps a bright band along the string.
Custom effects
Pass a function instead of a preset name. It receives each unit's local progress and a context
object, and returns inline style (and/or replacement content). Everything the presets do, you can
do — here a per-character blur-and-rise entrance.
For continuous effects, read ctx.globalProgress (the raw, un-staggered driver value) and
ctx.index instead of the first argument — that is how wave and shimmer phase each character.
Multiple lines
Use \n for explicit line breaks. Words still wrap within each line, and the stagger flows across
every line in reading order.
RTL
Every Appica UI component supports right-to-left layouts out of the box. Set the dir attribute on a
container (commonly your <html> element) so CSS logical properties resolve correctly, and wrap your
tree in DirectionProvider so direction-aware behavior follows the same direction.
Word and line units follow the resolved direction automatically. For setup details and caveats, see the RTL guide.
Use word or line splitting for cursive scripts
by="char" wraps every character in its own element, which breaks the contextual shaping of cursive scripts like
Arabic (letters stop joining). For Arabic, Persian, and similar scripts, use by="word" or by="line" so each shaped
unit stays intact — as the demo below does.
API reference
Renders a <span> wrapping the animated text. The full string is always present as an sr-only copy
for assistive tech; the animated glyphs are aria-hidden.
Forwards ref, className, and any other <span> attributes. The root is inline-block — override
display via className (e.g. block) if you need it.
The effect function
A custom effect is (progress, ctx) => { style?, className?, content? }, called once per unit per
frame:
Return style/className to animate the glyph and/or content to replace the displayed text (how
scramble and typewriter mutate characters).
Accessibility
- The complete text is always rendered as an
sr-onlycopy, so screen readers announce it once, in full — regardless of animation state. The animated glyphs arearia-hidden. - The animation honours
prefers-reduced-motion(andReducedMotionProvider): the built-in clock snaps straight to the final frame, the caret stops blinking, and motion-heavy presets (wave,flip,shimmer) render their static, fully-legible state. - A controlled
progressis not overridden by reduced motion — a scroll-linked reveal keeps working — but the motion-heavy presets still drop their transforms for those users. by="char"breaks the contextual shaping of cursive scripts (e.g. Arabic); useby="word"orby="line"there.- Because every unit re-renders each frame, keep animated strings to headline/sentence length rather than long bodies of text.