value is displayed exactly as it's given. A number goes through String(), and a string is shown character for character. Formatting is up to you, and anything you format animates: symbols, separators, signs and units are glyphs like any other.
Numbers as strings
String() drops trailing zeros, so 1.10 shows as 1.1, and a price that moves from 1.10 to 1.15 changes length. Fix the number of decimals with toFixed when they should always be there:
<NumericText value={price.toFixed(2)} monospacedDigits />Currency
Intl.NumberFormat does the grouping, the symbol and its placement:
const usd = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
<NumericText value={usd.format(balance)} fontSize={48} fontWeight="700" monospacedDigits />Usage
import { NumericText } from 'expo-content-transition';
import { useState } from 'react';
import { Pressable, View } from 'react-native';
const CURRENCIES = [
{ code: 'USD', locale: 'en-US', rate: 1 },
{ code: 'EUR', locale: 'de-DE', rate: 0.92 },
{ code: 'GBP', locale: 'en-GB', rate: 0.79 },
{ code: 'JPY', locale: 'ja-JP', rate: 149 },
];
const format = (usd: number, { code, locale, rate }: (typeof CURRENCIES)[number]) =>
new Intl.NumberFormat(locale, { style: 'currency', currency: code }).format(usd * rate);
export function Balance() {
const [balance, setBalance] = useState(2480.5);
const [index, setIndex] = useState(0);
const currency = CURRENCIES[index];
return (
<View style={{ alignItems: 'center', gap: 16 }}>
<Pressable onPress={() => setIndex((i) => (i + 1) % CURRENCIES.length)}>
<NumericText value={currency.code} fontSize={12} color="#6E6E73" direction="up" />
</Pressable>
<NumericText
value={format(balance, currency)}
decimalSeparator={currency.locale === 'de-DE' ? ',' : '.'}
fontSize={56}
fontWeight="700"
monospacedDigits
/>
{/* Deposit and withdraw buttons call setBalance */}
</View>
);
}Locales
Many locales use a comma for the decimal point and a dot or space for grouping. Tell NumericText which character is the decimal separator, or it will line digits up around the wrong one. Reading it from the formatter keeps the two in agreement:
function decimalSeparator(locale: string) {
return new Intl.NumberFormat(locale).formatToParts(1.1).find((part) => part.type === 'decimal')?.value ?? '.';
}
const format = new Intl.NumberFormat(locale, { style: 'currency', currency });
<NumericText value={format.format(total)} decimalSeparator={decimalSeparator(locale)} />Compact numbers
Compact notation turns 1,480 into 1.5K. When a value crosses a boundary, like 999 to 1K, the whole string changes shape and every glyph transitions:
Usage
import { NumericText } from 'expo-content-transition';
import { useState } from 'react';
import { Pressable, View } from 'react-native';
import { Heart } from './icons';
const compact = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 1 });
export function LikeButton() {
const [count, setCount] = useState(996);
const [liked, setLiked] = useState(false);
const toggle = () => {
setLiked((value) => !value);
setCount((value) => value + (liked ? -1 : 1));
};
return (
<Pressable onPress={toggle} style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}>
<Heart filled={liked} />
<NumericText value={compact.format(count)} fontSize={20} fontWeight="600" monospacedDigits />
</Pressable>
);
}Percentages and units
Suffixes such as %, km or bpm stay put while the digits in front of them change. For values that update many times a second, a shorter duration keeps the motion from lagging behind:
Usage
import { NumericText } from 'expo-content-transition';
import { View } from 'react-native';
export function Progress({ percent }: { percent: number }) {
return (
<View style={{ gap: 12 }}>
<NumericText
value={`${Math.round(percent)}%`}
duration={260}
fontSize={44}
fontWeight="600"
monospacedDigits
/>
<View style={{ height: 6, borderRadius: 3, backgroundColor: '#E5E5EA' }}>
<View style={{ width: `${percent}%`, height: 6, borderRadius: 3, backgroundColor: '#111' }} />
</View>
</View>
);
}Signs
Put the sign in the string. A change from +1.2% to -0.4% transitions the sign like any other glyph. Using the typographic minus (−, U+2212) keeps it the same width as + in most fonts, and NumericText reads it as a negative number when working out the direction.
const signed = `${change >= 0 ? '+' : '−'}${Math.abs(change).toFixed(2)}%`;Times
Pad times so their length doesn't change, and force the direction, since a clock's digits don't always grow as time goes forward:
Usage
import { NumericText } from 'expo-content-transition';
import { useEffect, useState } from 'react';
const now = () =>
new Date().toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit', second: '2-digit' });
export function Clock() {
const [time, setTime] = useState(now);
useEffect(() => {
const timer = setInterval(() => setTime(now()), 1000);
return () => clearInterval(timer);
}, []);
return <NumericText value={time} direction="up" fontSize={56} fontWeight="500" monospacedDigits />;
}Words
Any string works. Text with no digits is matched from its first letter, so a label changes in place:
<NumericText value={isSaving ? 'Saving' : 'Saved'} direction="up" />