High-performance split text animations for React Native, powered by Reanimated and extensible renderers.
Korean documentation: README.ko.md
Package documentation: packages/text-motion/README.md
This repository is a pnpm workspace for the @react-native-motion-kit/text-motion package and Expo example app.
text-motion is not a single prop-heavy wrapper like <AnimatedText animation="fadeIn" />. It helps you create reusable animated text components by choosing how the text is split, when each visible token starts, and which effects are applied.
The package exposes that as a recipe pipeline:
split -> layout(renderer) -> timeline -> effect -> motion/accessibility -> componentUse presets when you want a good default quickly. Compose a recipe when you need to control tokenization, timing, effects, accessibility, or renderer choice.
For TypeScript users, .component() is only available after a renderer is selected with .layout(...). .recipe() can still be used before layout when you need to inspect or pass around an unfinished recipe.
Quick preset usage:
import { editorialRise } from '@react-native-motion-kit/text-motion/presets';
const HeroReveal = editorialRise().component();
export function HeroTitle() {
return <HeroReveal>Design motion that feels native</HeroReveal>;
}Custom recipe usage:
import {
defineTextMotion,
fade,
nativeText,
rise,
stagger,
words,
} from '@react-native-motion-kit/text-motion';
const WordReveal = defineTextMotion()
.split(words())
.layout(nativeText())
.timeline(stagger(0.034))
.effect(fade().and(rise({ y: 10 })))
.component();Playback controls are useful when an event should tell a text motion component to play, replay, reset, or stop without remounting. In that mode the app owns the event, while the component still owns playback execution and uses its recipe .motion() config.
Basic controls usage:
import {
defineTextMotion,
fade,
nativeText,
rise,
stagger,
useTextMotionControls,
words,
} from '@react-native-motion-kit/text-motion';
import { Button } from 'react-native';
const ReplayableReveal = defineTextMotion()
.split(words())
.layout(nativeText())
.timeline(stagger(0.04))
.effect(fade().and(rise({ y: 12 })))
.motion({ kind: 'timing', options: { duration: 420 } })
.component();
export function Headline() {
const controls = useTextMotionControls();
return (
<>
<ReplayableReveal controls={controls}>
Replay without remounting
</ReplayableReveal>
<Button title="Replay" onPress={controls.replay} />
</>
);
}controls is a command channel, not a progress value. Use it for buttons, screen focus, onboarding steps, example replay controls, and coordinated title/subtitle replay.
Performance note: controls are intended for short UI text such as titles, labels, and product copy. If you plan to animate long paragraphs, grapheme-split copy, or many rows at once, measure in your target app before treating that as a supported workload.
Controlled progress is useful when text motion should follow a raw value outside the component, such as scroll position, gesture progress, or another Reanimated shared value. In that mode the app owns the exact progress and the text component only renders the current state.
Basic controlled progress usage:
import {
defineTextMotion,
fade,
nativeText,
rise,
stagger,
words,
} from '@react-native-motion-kit/text-motion';
import { Button } from 'react-native';
import { useSharedValue, withTiming } from 'react-native-reanimated';
const ControlledReveal = defineTextMotion()
.split(words())
.layout(nativeText())
.timeline(stagger(0.08))
.effect(fade().and(rise({ y: 12 })))
.component();
export function Headline() {
const progress = useSharedValue(0);
return (
<>
<ControlledReveal progress={progress}>
Progress drives the whole reveal
</ControlledReveal>
<Button title="Play" onPress={() => (progress.value = withTiming(1))} />
</>
);
}progress is normalized whole-text progress: 0 is initial and 1 is final. Timeline delays still map that global value to each token's local progress, so stagger() and wave() keep their timing shape. This is handy when a headline should reveal as a user scrolls a card into view, when text should follow a gesture, or when several UI elements need to share one progress value.
When progress is provided, .motion() does not run internal autoplay. Choose the playback feel where you update the shared value instead, for example progress.value = withTiming(1, { duration: 720 }) or progress.value = withSpring(1). controls and progress cannot be used together.
- recipe API:
defineTextMotion().split().layout().timeline().effect().component() - splitters:
graphemes,words,custom, experimental newline-onlylines - renderer:
nativeText - timelines:
stagger,sequence,parallel,wave - effects:
fade,rise,slide,scale,pulse,shake - presets subpath:
@react-native-motion-kit/text-motion/presets - explicit playback
controlsfor event-driven play/replay/reset/stop - controlled progress via external Reanimated shared values
- accessibility default: parent label plus hidden decorative token nodes
nativeText() is a transform-first token renderer. It uses a wrapping View with animated Text tokens so transform effects are visible in React Native. It is not a full React Native Text drop-in, and layout props such as numberOfLines, ellipsizeMode, and onTextLayout are intentionally outside the stable MVP contract.
Manual hard line breaks are supported: text such as "First line\nSecond line" keeps
the newline as a visual break in nativeText(), and the newline does not consume a
motion index. Automatic RN-rendered line-to-token mapping is still deferred.
By default, playback is lifecycle-driven: animated tokens autoplay on mount, ordinary parent rerenders with the same text/recipe preserve in-flight progress, and text/effect/timeline/motion changes replay the affected token animation. Text changes are enter-only in the MVP: old text is not kept for exit animation, crossfade, or token diffing. Components also accept controls for event-driven playback and progress, a Reanimated SharedValue<number> from 0 to 1, for raw externally controlled progress. controls uses the component recipe .motion() against the current text; progress is app-owned and makes changed text follow the current shared value instead of starting autoplay.
Skia, stable line reveal, rich text, and RN-rendered line-to-token mapping are deferred. Context/provider playback wiring and public playback refs are intentionally not part of the design; pass controls explicitly where a component should respond to playback commands.
text-motion stays focused on split text motion: tokenization, layout-aware timing, effects, renderers, split-token playback, accessibility, and presets for titles, labels, onboarding copy, and product copy.
Features that depend on text tokens or text layout can be considered here, including typewriter, scramble, wipe, text-change transitions, and renderer-specific effects behind explicit capability boundaries. Precise line-aware reveal remains deferred until RN layout measurement and token-to-line mapping policies are reliable enough to document.
Features that solve a different problem are likely better served by separate package candidates. Number count-up, odometer, currency, percentage, timer, and delta animations are value-formatting problems rather than split-text layout problems. A separate value-motion package could share Motion Kit conventions without expanding the text-motion API surface.
Decision rule: add a feature to text-motion only when it improves text-specific production work without making the common recipe API harder to understand.
pnpm add @react-native-motion-kit/text-motion react-native-reanimated react-native-workletsThe MVP peer dependency minimums are react-native-reanimated@>=4.0.0 and react-native-worklets@>=0.5.0. Follow the official Reanimated setup for New Architecture and Worklets configuration.
react-native-text-motion/
example/
packages/
text-motion/corepack pnpm install
corepack pnpm run typecheck
corepack pnpm run test
corepack pnpm run build
corepack pnpm run example:buildUseful package-level docs:
Use this package today when you need:
- entrance motion for short React Native text such as titles, labels, onboarding copy, or product copy
- word-level or grapheme-level reveal effects powered by Reanimated
- reusable motion recipes instead of one-off animation props
- accessible split text that screen readers read as one phrase
- event-driven playback controls for replay/reset/stop without remounting
- externally controlled raw progress for scroll, gesture, or synchronized text motion
- a lightweight core package without Skia as a required dependency
Wait for a later version if you need:
- playback APIs such as
pause,seek, orreverse - first-class scroll, gesture, or in-view drivers
- precise line reveal, masked reveal, or token-to-line mapping
- Skia effects such as blur, glow, shaders, masks, or glyph distortion
- rich nested text with links, selectable text, or full native
Textlayout parity - heavy use across long paragraphs, virtualized lists, or dense UI without your own performance validation
Next focus: proving controls in real app flows, then deciding whether state-transition props, pause/seek/reverse, in-view helpers, scroll/gesture drivers, line-aware effects, or optional Skia renderers deserve first-class APIs.
MIT