
Animated number transitions where each digit rolls in parallel like an odometer; customizable springs, direction and stagger; preserves locale digits, leaves formatting to caller, exposes formatted value for accessibility.
Animated number transitions for Compose Multiplatform. Every digit rolls in parallel, like an odometer — never by counting through the values in between.
The same value change under four different springs. Notice the columns move together, and that each one takes the shortest path to its target rather than counting.
implementation("io.github.atul-khandekar:compose-number-flow:0.1.0")Give it a value and change that value. There is nothing to start, stop or reset.
var count by remember { mutableStateOf(0) }
NumberFlow(
value = count,
style = MaterialTheme.typography.displayLarge,
)
Button(onClick = { count += 1_000 }) { Text("Add") }There are String, Int and Long overloads. The String one is the real API — reach for it
whenever the number needs separators, a currency symbol or decimals.
NumberFlow is deliberately Material-agnostic, so it does not depend on Material and does not
read LocalTextStyle. Pass a style yourself:
NumberFlow(value = count, style = MaterialTheme.typography.displayLarge) // or
NumberFlow(value = count, style = LocalTextStyle.current)Without one you get TextStyle.Default — small black text, invisible on a dark background.
This is the one thing that catches people out.
The library animates characters; it does not format numbers. Format first, then hand it over:
val formatted = NumberFormat.getCurrencyInstance().format(amount) // "$1,234.56"
NumberFlow(value = formatted, style = MaterialTheme.typography.displayLarge)Digits roll; separators, currency symbols and signs do not — they fade and slide when the number changes length, and the row's width animates with them.
Digits from any decimal script work, and each column rolls in the script it was given:
"٥٤٣" rolls through Arabic-Indic glyphs, not Western ones. That matters because locale-aware
formatters emit exactly these characters.
Everything lives on NumberFlowConfig:
| Parameter | Type | Default | What it does |
|---|---|---|---|
animationSpec |
AnimationSpec<Float> |
spring(dampingRatio = 0.8f, stiffness = Low) |
Drives each column's roll. Any Compose spec works. |
direction |
NumberFlowDirection |
AUTO |
AUTO takes the shortest path around 0–9; UP and DOWN force one way. |
staggerDelayMillis |
Long |
0 |
Delay between consecutive columns starting. Non-zero switches stagger on. |
staggerOrder |
StaggerOrder |
RIGHT_TO_LEFT |
Which end leads. Ignored while the delay is zero. |
NumberFlow(
value = score,
style = MaterialTheme.typography.displayMedium,
config = NumberFlowConfig(
animationSpec = spring(dampingRatio = 0.5f),
direction = NumberFlowDirection.UP,
staggerDelayMillis = 60L,
),
)Or take a preset:
| Preset | Feel |
|---|---|
NumberFlowDefaults.Config |
The default spring — settled, with a touch of overshoot. |
NumberFlowDefaults.snappy() |
Critically damped and quick. No overshoot. |
NumberFlowDefaults.bouncy() |
Loose and playful, with visible overshoot. |
NumberFlowDefaults.smooth() |
A 600ms tween, for when a roll must land on a known beat. |
Tabular figures (tnum) are applied for you so the digits cannot jiggle mid-roll — unless you
set fontFeatureSettings yourself, in which case yours wins.
That is the entire public API: three NumberFlow overloads, one config class, two enums and a
defaults object.
A rolling-number component can animate serially — counting from 1,234 up through every value to 5,678 — or in parallel, giving each digit position its own spring so all the columns start and finish together. This library does the latter: the thousands digit is not waiting on the ones digit.
It also runs everywhere Compose does. The whole library is commonMain, with no expect/actual
and no platform imports, so Android, iOS, desktop and the browser share one implementation.
A few details that matter in practice:
contentDescription, so screen
readers announce the number rather than spelling out digits.Android (minSdk 24) · iOS (arm64 + simulator) · Desktop (JVM) · Wasm
iosX64 is not supported, because Compose Multiplatform no longer publishes artifacts for it.
./gradlew :demo:run # desktop
./gradlew :androidApp:installDebug # android
./gradlew :demo:wasmJsBrowserDevelopmentRun # browser
open iosApp/iosApp.xcodeproj # iosTo change the library itself and see the effect in your own app without publishing, point your app's build at a local checkout:
// settings.gradle.kts in your app
includeBuild("../compose-number-flow")Gradle substitutes the local sources for the published dependency automatically — no version
bumps, no mavenLocal().
./gradlew :number-flow:build # all targets, all testsInspired by NumberFlow by Max Barvian — this is the Compose equivalent of the same idea.
Apache 2.0. See LICENSE.
Animated number transitions for Compose Multiplatform. Every digit rolls in parallel, like an odometer — never by counting through the values in between.
The same value change under four different springs. Notice the columns move together, and that each one takes the shortest path to its target rather than counting.
implementation("io.github.atul-khandekar:compose-number-flow:0.1.0")Give it a value and change that value. There is nothing to start, stop or reset.
var count by remember { mutableStateOf(0) }
NumberFlow(
value = count,
style = MaterialTheme.typography.displayLarge,
)
Button(onClick = { count += 1_000 }) { Text("Add") }There are String, Int and Long overloads. The String one is the real API — reach for it
whenever the number needs separators, a currency symbol or decimals.
NumberFlow is deliberately Material-agnostic, so it does not depend on Material and does not
read LocalTextStyle. Pass a style yourself:
NumberFlow(value = count, style = MaterialTheme.typography.displayLarge) // or
NumberFlow(value = count, style = LocalTextStyle.current)Without one you get TextStyle.Default — small black text, invisible on a dark background.
This is the one thing that catches people out.
The library animates characters; it does not format numbers. Format first, then hand it over:
val formatted = NumberFormat.getCurrencyInstance().format(amount) // "$1,234.56"
NumberFlow(value = formatted, style = MaterialTheme.typography.displayLarge)Digits roll; separators, currency symbols and signs do not — they fade and slide when the number changes length, and the row's width animates with them.
Digits from any decimal script work, and each column rolls in the script it was given:
"٥٤٣" rolls through Arabic-Indic glyphs, not Western ones. That matters because locale-aware
formatters emit exactly these characters.
Everything lives on NumberFlowConfig:
| Parameter | Type | Default | What it does |
|---|---|---|---|
animationSpec |
AnimationSpec<Float> |
spring(dampingRatio = 0.8f, stiffness = Low) |
Drives each column's roll. Any Compose spec works. |
direction |
NumberFlowDirection |
AUTO |
AUTO takes the shortest path around 0–9; UP and DOWN force one way. |
staggerDelayMillis |
Long |
0 |
Delay between consecutive columns starting. Non-zero switches stagger on. |
staggerOrder |
StaggerOrder |
RIGHT_TO_LEFT |
Which end leads. Ignored while the delay is zero. |
NumberFlow(
value = score,
style = MaterialTheme.typography.displayMedium,
config = NumberFlowConfig(
animationSpec = spring(dampingRatio = 0.5f),
direction = NumberFlowDirection.UP,
staggerDelayMillis = 60L,
),
)Or take a preset:
| Preset | Feel |
|---|---|
NumberFlowDefaults.Config |
The default spring — settled, with a touch of overshoot. |
NumberFlowDefaults.snappy() |
Critically damped and quick. No overshoot. |
NumberFlowDefaults.bouncy() |
Loose and playful, with visible overshoot. |
NumberFlowDefaults.smooth() |
A 600ms tween, for when a roll must land on a known beat. |
Tabular figures (tnum) are applied for you so the digits cannot jiggle mid-roll — unless you
set fontFeatureSettings yourself, in which case yours wins.
That is the entire public API: three NumberFlow overloads, one config class, two enums and a
defaults object.
A rolling-number component can animate serially — counting from 1,234 up through every value to 5,678 — or in parallel, giving each digit position its own spring so all the columns start and finish together. This library does the latter: the thousands digit is not waiting on the ones digit.
It also runs everywhere Compose does. The whole library is commonMain, with no expect/actual
and no platform imports, so Android, iOS, desktop and the browser share one implementation.
A few details that matter in practice:
contentDescription, so screen
readers announce the number rather than spelling out digits.Android (minSdk 24) · iOS (arm64 + simulator) · Desktop (JVM) · Wasm
iosX64 is not supported, because Compose Multiplatform no longer publishes artifacts for it.
./gradlew :demo:run # desktop
./gradlew :androidApp:installDebug # android
./gradlew :demo:wasmJsBrowserDevelopmentRun # browser
open iosApp/iosApp.xcodeproj # iosTo change the library itself and see the effect in your own app without publishing, point your app's build at a local checkout:
// settings.gradle.kts in your app
includeBuild("../compose-number-flow")Gradle substitutes the local sources for the published dependency automatically — no version
bumps, no mavenLocal().
./gradlew :number-flow:build # all targets, all testsInspired by NumberFlow by Max Barvian — this is the Compose equivalent of the same idea.
Apache 2.0. See LICENSE.