NitroNumber
The only thing the view needs is a number. Every change to value is one native animation: each digit takes the shortest path to its new glyph in the direction of the change, columns that appear slide in from blank and columns that disappear slide out. A value that arrives mid-roll re-targets the running roll instead of restarting it, so a ticker that updates several times a second keeps flowing.
Timing
duration is the length of a roll in milliseconds (0 snaps). easing picks the curve: linear, easeIn, easeOut, easeInOut (default) or spring, whose overshoot is bounce (0–1). stagger delays each digit's start, least significant first, so a change cascades like a mechanical carry. By default a digit rolls up when the number grows and down when it shrinks; direction="up" or "down" forces one, for a countdown that should always fall.
- Preview
- Code
<NitroNumber
value={value}
groupingSeparator=","
fontSize={56}
fontWeight="800"
easing="spring" // 'linear' | 'easeIn' | 'easeOut' | 'easeInOut' | 'spring'
bounce={0.15} // spring overshoot, 0–1
duration={500} // ms
stagger={30} // ms between digits, least significant first
/>
Transitions
transition picks how a change plays. Besides the roll, two transitions swap each changed glyph in place: digits that don't change stay put, and the change cascades from the leftmost changed digit (stagger). Under Reduce Motion every transition snaps.
numericis SwiftUI's.contentTransition(.numericText()): the old glyph softens, shrinks and slides out while the new one slides in from the other side, small and out of focus, and resolves. The pacing is SwiftUI's, measured frame by frame and scaled toduration;easingandbounceare not consulted. The blur is a blurred copy of each glyph rendered once per font and cross-faded with the sharp one, so a frame costs what a frame of a roll does.scramble: each changed digit shows a random digit every few frames and locks on its target, from the left.
The text around the digits changes the same way. A new prefix, suffix or separator softens out while the new one comes into focus, and its width eases between the two. A change of fractionDigits plays too: the digits keep their place value, decimal columns that go close and new ones open blank (swapping in, or rolling up from blank in a roll), and the decimal separator fades with them. A currency switch ("11.459,69 €" to "£9,587.05" to "¥1,856,853") is one transition, however fast the switches come.
- Preview
- Code
<NitroNumber
value={value}
transition="numeric" // 'roll' | 'numeric' | 'scramble'
groupingSeparator=","
fontSize={56}
fontWeight="800"
/>
The change flash and the pop
With any transition, flashUpColor and flashDownColor light every changed digit in the up colour when the value grew and the down colour when it shrank, fading back over flashDuration (600 ms) once it has landed: a trading screen's change flash. popOnChange punches the whole figure on every change, its peak overshoot as a fraction of the size; 0.08 is a nudge, 0.2 a slam.
- Preview
- Code
<NitroNumber
value={price}
fractionDigits={2}
groupingSeparator=","
prefix="$"
flashUpColor="#16a34a"
flashDownColor="#dc2626"
popOnChange={0.08}
/>
Signs, cents and affixes
signDisplay puts a plus on gains, as Intl.NumberFormat's does, and a plus that turns into a minus swaps like any other glyph; fractionFontSize, fractionColor and fractionAlign set the cents apart, and prefixColor and suffixColor mute a currency symbol or a unit.
<NitroNumber
value={change}
format={new NumberFormat('en-US', { style: 'currency', currency: 'USD', signDisplay: 'exceptZero' })}
color={change >= 0 ? '#16a34a' : '#dc2626'}
/>
<NitroNumber
value={balance}
fractionDigits={2}
groupingSeparator=","
prefix="$"
prefixColor="#9ca3af"
fractionFontSize={24}
fractionColor="#9ca3af"
fractionAlign="top"
/>
Clocks and timers
NitroTime shows a number of seconds as a clock, a timer or a countdown. The tens of seconds and of minutes wrap after 5, as a clock's wheels do, so 0:59 → 1:00 turns them one step instead of rolling back through 4, 3, 2, 1.
import { NitroTime } from 'react-native-nitro-input'
<NitroTime seconds={elapsed} /> // 12:07
<NitroTime seconds={remaining} timeFormat="h:mm:ss" /> // 1:00:05
It is a NitroNumber underneath, and takes its props. The same wheels are open to any figure through digits (a position's highest digit) and groupingSizes.
Compact figures
A compact format rolls the figure it shows and swaps its suffix: "950" → "1.5K" → "2.3M".
<NitroNumber value={followers} format={new NumberFormat('en-US', { notation: 'compact' })} />
Native digits and grouping
A format in a locale with its own digits or grouping brings them: en-IN groups 12,34,567 and ar-EG draws ٠١٢٣٤٥٦٧٨٩. Without a format, groupingSizes and digitGlyphs say the same.
Continuous rolls
With continuous, the wheels below the highest one that changes turn a full turn too, so 100 → 200 seems to pass through every value between, as NumberFlow's continuous does.
Animation events
onAnimationStart fires when the figure sets off from rest and onAnimationEnd when it comes to rest, once for a run of changes that arrived while it was moving: the moment for a haptic, or for what follows the figure.
<NitroNumber value={total} onAnimationEnd={() => Haptics.selectionAsync()} />
Imperative
- Preview
- Code
const ref = useRef<NitroNumberHandle>(null)
<NitroNumber ref={ref} value={42} fractionDigits={1} minimumIntegerDigits={4} />
ref.current?.animateTo(9999) // animates, like changing the prop
ref.current?.jumpTo(999.75) // positions the wheels continuously, no animation
ref.current?.revealTo(1234.5) // plays a jackpot reveal
ref.current?.getValue() // the value shown or being animated towards
// scrubbing from a scroll handler
<ScrollView onScroll={(e) => ref.current?.jumpTo(e.nativeEvent.contentOffset.x / 4)} scrollEventThrottle={16} />
jumpTo positions every wheel from a continuous number, the way a mechanical odometer does: 41.75 shows the units wheel three quarters of the way from 1 to 2, and a wheel only turns while every lower wheel is on its way from 9 to 0. That is what a scroll, drag or gesture wants. It also skips a running reveal.
jumpTo and animateTo are safe to call from any thread and coalesce: only the newest value per main-thread turn is applied, so pushing a value every frame into many views never builds a backlog.
From a Reanimated worklet, call them on the Nitro object (onNativeRef) directly: a Nitro object can be captured by a worklet as it is. A feed that runs on the UI thread keeps the figure moving while the JS thread is busy:
const [price, setPrice] = useState<NitroNumberRef>()
useFrameCallback((frame) => {
'worklet'
if (price && tickDue(frame)) price.animateTo(nextPrice())
}, true)
<NitroNumber value={initial} onNativeRef={setPrice} />
Hand the final value back to React once the feed stops (with scheduleOnRN) so the value prop matches what is on screen; posting every tick instead queues them up while JS is busy and replays them afterwards.