Description:
expo-content-transition is a React Native and Expo component that animates changing text through a native NumericText view. It rolls, scales, blurs, and staggers individual glyphs.
Numeric values align around the decimal separator, so a small change can animate only the characters that changed.
See it in action
Features
- Rolls, scales, blurs, and staggers individual glyphs.
- Aligns numeric characters around a configurable decimal separator.
- Keeps shared prefixes fixed when the displayed content is non-numeric.
- Supports automatic, upward, and downward roll direction.
- Adds bounce, entry scale, travel distance, per-glyph blur, clipping, and instant updates.
- Uses monospaced digit columns when neighboring values must keep their positions.
- Blends frequent value changes into the current transition.
- Accepts font, color, alignment, spacing, and React Native text styles.
How To Use It
Install and Build the Native Module
bun install expo-content-transition
bunx expo prebuild
bunx expo run:ios
# Or use bunx expo run:androidRun expo run:ios or expo run:android again after installing into a project that already has native folders.
Basic Numeric Counter
Pass a number or string through value. The first value renders immediately. Later changes use the configured transition.
import { useState } from "react";
import { Pressable, Text, View } from "react-native";
import { NumericText } from "expo-content-transition";
export default function ScoreCard() {
const [score, setScore] = useState(128);
return (
<View>
<NumericText
value={score}
color="#F8FAFC"
fontSize={64}
fontWeight="700"
monospacedDigits
/>
<Pressable onPress={() => setScore((current) => current + 1)}>
<Text>Increment</Text>
</Pressable>
</View>
);
}Animate a Stopwatch
A fast-changing string keeps its time separators in place. The component blends updates that arrive before an earlier transition finishes.
import { useEffect, useState } from "react";
import { Pressable, Text, View } from "react-native";
import { NumericText } from "expo-content-transition";
function pad(value: number) {
return String(value).padStart(2, "0");
}
export default function Stopwatch() {
const [running, setRunning] = useState(false);
const [ticks, setTicks] = useState(0);
useEffect(() => {
if (!running) return;
const timer = setInterval(() => {
setTicks((current) => current + 1);
}, 100);
return () => clearInterval(timer);
}, [running]);
const display = `${pad(Math.floor(ticks / 600))}:${pad(Math.floor(ticks / 10) % 60)}.${ticks % 10}`;
return (
<View>
<NumericText
value={display}
color="#E2E8F0"
fontSize={48}
fontWeight="500"
monospacedDigits
/>
<Pressable onPress={() => setRunning((current) => !current)}>
<Text>{running ? "Stop" : "Start"}</Text>
</Pressable>
</View>
);
}Numeric Alignment and Text Styling
Numeric strings align around decimalSeparator. The default separator is a period. Set another character when the displayed value uses a different convention.
monospacedDigits assigns uniform widths to digit columns. Use it for counters and timers where a changing digit must not shift neighboring columns.
The base text props:
| Prop | Type | Default | Purpose |
|---|---|---|---|
color | string | Platform color | Sets the text color. |
fontSize | number | Not specified | Sets the size in scale-independent pixels. |
fontWeight | FontWeight | normal | Accepts normal, bold, or 100 through 900. |
fontStyle | FontStyle | normal | Accepts normal or italic. |
fontFamily | string | Not specified | Resolves Expo fonts, bundled fonts, or platform fonts like React Native Text. |
letterSpacing | number | Not specified | Adds character spacing in points. |
monospacedDigits | boolean | false | Keeps digit columns at uniform widths. |
alignment | Alignment | start | Accepts start, center, or end. |
decimalSeparator | string | . | Sets the character used to align whole and fractional parts. |
style | TextStyle | Not specified | Applies text props to glyphs and other style values to the view. |
Transition Props
Pass these props when the default motion does not match the interface.
| Prop | Type | Default | Purpose |
|---|---|---|---|
direction | Direction | auto | Rolls up as the value grows and down as it shrinks. Use up or down to force a direction. |
duration | number | 420 | Sets the nominal duration in milliseconds and scales the internal spring. |
bounce | number | 0.46 | Sets roll overshoot from 0 to 0.95. |
enterScale | number | 0.4 | Sets the entry size of a new glyph. Use 1 for a pure roll. |
travel | number | 0.333 | Sets vertical travel as a fraction of line height. Use 0 to remove vertical movement. |
blur | boolean | true | Blurs each glyph during its transition. |
blurIntensity | number | 1 | Scales blur from 0 to 8. A value of 0 matches blur={false}. |
maxBlurRadius | number | Unbounded | Sets a ceiling for the blur radius in dp. |
clip | boolean | true | Clips each glyph to its line box. |
animated | boolean | true | Applies updates immediately when set to false. |
FAQs
Q: Can expo-content-transition run in Expo Go?
A: No. It contains native iOS and Android code. Use a development build or a bare workflow after prebuild.
Q: What happens on the Web?
A: The Web implementation falls back to a plain React Native Text component. It does not animate transitions.
Q: Why do digits shift when the value changes?
A: Set monospacedDigits to true for fixed-width digit columns. Set alignment to keep the whole value anchored at start, center, or end.
Q: Why is blur missing on an Android device?
A: The blur effect requires Android 12, API 31 or newer. Lower API levels ignore blur.
Q: How do I disable motion for a final state or snapshot?
A: Pass animated={false}. The first value is immediate even when animation is enabled.





