Skip to content

Repository files navigation

compose-number-flow

Maven Central Check License

Animated number transitions for Compose Multiplatform. Every digit rolls in parallel, like an odometer — never by counting through the values in between.

compose-number-flow

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.

Install

implementation("io.github.atul-khandekar:compose-number-flow:0.1.0")

Usage

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.

It does not read your theme

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.

Formatting is yours

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.

Customization

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.

How it compares

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:

  • Wrapping has no seam. Each column animates an unbounded position on a conceptually infinite strip, so 9 → 0 is just "one step up" and needs no special case.
  • Rolls stay in the draw phase. Two glyphs are painted straight onto a canvas, and the animated value is read inside the draw lambda, so a roll never triggers recomposition.
  • Interruption redirects. Change the value mid-roll and the spring picks up the current velocity instead of snapping.
  • Accessibility. The formatted value is exposed as the row's contentDescription, so screen readers announce the number rather than spelling out digits.

Targets

Android (minSdk 24) · iOS (arm64 + simulator) · Desktop (JVM) · Wasm

iosX64 is not supported, because Compose Multiplatform no longer publishes artifacts for it.

Demos

./gradlew :demo:run                              # desktop
./gradlew :androidApp:installDebug               # android
./gradlew :demo:wasmJsBrowserDevelopmentRun      # browser
open iosApp/iosApp.xcodeproj                     # ios

Working on the library

To 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 tests

Credits

Inspired by NumberFlow by Max Barvian — this is the Compose equivalent of the same idea.

License

Apache 2.0. See LICENSE.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages