A per-digit animated number ticker for Compose Multiplatform — only the digits that change move, and they roll up when the value increases, down when it decreases. Locale-friendly formatting, configurable animation, prefix/suffix friendly.
Animating a number is almost easy in Compose — AnimatedContent will cross-fade 1247 to
1248. But the eye notices: only the 7 should move, not the whole number. NumberFlow does that:
per-digit slide, with the direction inferred from whether the value went up or down. It's the
satisfaction that makes counters, scoreboards, prices and stat dashboards feel polished — written
once, working on every CMP target.
| Platform | Supported | Tested |
|---|---|---|
| Android | ✅ | ✅ (unit + UI) |
| iOS | ✅ | ✅ (UI, Skiko) |
| Desktop | ✅ | ✅ (unit + UI) |
| Web | ✅ | ✅ (compile + logic) |
gradle/libs.versions.toml:
[libraries]
number-flow = { module = "io.github.nadeemiqbal:number-flow", version = "0.1.0" }commonMain dependencies:
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.number.flow)
}
}
}var score by remember { mutableStateOf(1247L) }
NumberFlow(
value = score,
format = NumberFlowDefaults.thousandsLong(),
)Every value change animates only the digits that actually changed.
Counter that increases over time
var count by remember { mutableStateOf(0L) }
LaunchedEffect(Unit) {
while (true) { delay(700); count += 1 }
}
NumberFlow(value = count, format = NumberFlowDefaults.thousandsLong())Price with two decimal places
NumberFlow(
value = price,
format = NumberFlowDefaults.thousands(decimalPlaces = 2),
style = MaterialTheme.typography.displaySmall,
)Custom format (anything (Double) -> String)
NumberFlow(
value = balance,
format = { v -> "$" + NumberFlowDefaults.thousands(decimalPlaces = 2)(v) },
)Custom animation spec
NumberFlow(
value = score,
animationSpec = spring(dampingRatio = Spring.DampingRatioLowBouncy, stiffness = Spring.StiffnessLow),
)Int/Long convenience overloads
NumberFlow(value = 42) // Int
NumberFlow(value = 1_000_000L) // Long
NumberFlow(value = 3.14) // Doubleformat— full control over the displayed string.NumberFlowDefaults.thousands()andthousandsLong()cover the common cases (grouping + fixed decimal places). Bring your own for currency, percentages, units, prefixes/suffixes.animationSpec— anyFiniteAnimationSpec<IntOffset>. Defaults to a 450 ms tween.style/color— standardTextStyle+ colour overrides; the entire ticker shares one style and lays out as a single horizontal row.
NumberFlowInternal.kt exposes formatWithThousands(value, grouping, decimalSeparator, decimalPlaces)
and groupThousands(digits, grouping) as internal functions — used by the composable, and
covered by the test suite. If you have your own number-format logic, plug it in via format.
| NumberFlow | AnimatedContent<Number> |
Plain Text("$value") |
|
|---|---|---|---|
| Per-digit animation | ✅ | ❌ whole-string fade | ❌ no animation |
| Direction-aware (up vs down) | ✅ | ❌ | |
| Locale-free thousands grouping | ✅ | ||
| Custom format hook | ✅ | ✅ | ✅ |
| Multiplatform | ✅ A/iOS/Desktop/Web | ✅ | ✅ |
- Optional staggered per-digit delay
- Spring-physics defaults
- Locale-aware decimal/grouping resolution via
expect/actual
See CONTRIBUTING.md. Bug reports and feature requests are welcome via GitHub Issues.
Copyright 2026 Nadeem Iqbal
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
See LICENSE for the full text.
