number_flow_flutter 0.1.0
number_flow_flutter: ^0.1.0 copied to clipboard
Beautiful animated numbers for Flutter — digit-by-digit rolling transitions with locale-aware formatting, springs, and motion blur.
number_flow_flutter #

Beautiful, animated numbers for Flutter. When a value changes, each digit rolls to its new position like an odometer — with locale-aware formatting, smooth enter/exit transitions as the number grows or shrinks, an optional iOS-style spring, and velocity-based motion blur.
NumberFlow handles numbers (counters, currency, percentages, compact and
scientific notation); TimeFlow handles clocks, countdowns, and stopwatches.
Features #
- Digit-by-digit rolling — only the digits that changed animate; the rest stay put.
- Locale-aware formatting — decimal, currency, percent, compact (
1.2K), scientific (1.2×10³), and engineering notation, with correct grouping, numbering systems, and RTL. - Smooth reflow — digits slide to their new column and the box resizes when the number grows or shrinks; entering digits roll in, exiting digits fade out.
- Curve or spring — a front-loaded easing curve by default, or an opt-in iOS-like spring with overshoot and velocity hand-off for interrupted rolls.
- Motion blur — optional velocity-based blur while rolling (vertical and/or horizontal), fully tunable.
- Scrub mode — drive the display from a
ValueListenable<num>(a slider, chart scrubber, gesture) at high frequency without a rebuild per tick. - Accessible — exposes a plain-text semantics label; honors the platform reduce-motion setting.
Installation #
Add to your pubspec.yaml:
dependencies:
number_flow_flutter: ^0.0.1
Then:
import 'package:number_flow_flutter/number_flow_flutter.dart';
Quick start #
NumberFlow(value: 1234)
That's it — whenever value changes on rebuild, the digits animate to the new
number.
NumberFlow #
Formatting #
Pass a NumberFlowFormat to control how the value is rendered:
// Currency
NumberFlow(
value: 1234.56,
format: const NumberFlowFormat.currency(currencyCode: 'USD', symbol: r'$'),
locale: 'en_US',
)
// Percent (0.0–1.0 → 0%–100%)
NumberFlow(value: 0.42, format: const NumberFlowFormat.percent())
// Compact — 1,234,567 → 1.2M
NumberFlow(
value: 1234567,
format: const NumberFlowFormat.compact(maxFraction: 1),
)
// Scientific — 12345 → 1.23×10⁴ (the exponent renders as a superscript)
NumberFlow(
value: 12345,
format: const NumberFlowFormat.scientific(maxFraction: 2),
)
// Engineering — exponent is always a multiple of 3
NumberFlow(value: 1500000, format: const NumberFlowFormat.engineering())
// Plain decimal with fraction bounds
NumberFlow(
value: 3.14159,
format: const NumberFlowFormat.decimal(minFraction: 2, maxFraction: 2),
)
Every format accepts a sign (SignDisplay.auto, always, exceptZero,
negative, never) to control when the +/- sign shows.
Prefix, suffix, and trend #
NumberFlow(
value: score,
prefix: '~',
suffix: ' pts',
// Direction the digits roll. Omit to auto-detect from the value change.
trend: 1, // 1 up, -1 down, 0 shortest path
)
For a custom rule, pass trendFn: (prev, next) => ... instead of trend.
Styling and layout #
NumberFlow(
value: 1234,
style: const TextStyle(fontSize: 48, fontWeight: FontWeight.w600),
textAlign: TextAlignValue.end,
direction: Direction.auto, // ltr / rtl / auto
tabularNums: true, // uniform digit width — no horizontal shift
)
Animation #
The roll uses a fixed-duration easing curve by default. Customize it, or opt into a spring:
// Custom curve timing
NumberFlow(
value: v,
spinTiming: const TimingConfig(
duration: Duration(milliseconds: 700),
curve: NumberFlowCurve(),
),
)
// iOS-like spring (overshoot + velocity hand-off on interrupt)
NumberFlow(value: v, spring: NumberFlowSpring.ios)
// Custom spring
NumberFlow(
value: v,
spring: const NumberFlowSpring(mass: 1, stiffness: 170, damping: 20),
)
Other animation knobs:
| Property | Description |
|---|---|
animated |
Master switch; false snaps with no animation. |
respectMotionPreference |
When true (default), honors the OS reduce-motion setting. |
transformTiming |
Timing for horizontal reflow (digits sliding to new columns). |
opacityTiming |
Enter/exit fade timing. |
continuous |
Odometer mode: unchanged lower digits spin a full extra cycle on carry (needs a directional trend). |
stagger |
Left-to-right delay per digit (higher digits lead). |
wheelSpacing |
Vertical spacing between wheel glyphs (× line height). |
mask |
Fade the top/bottom edges of the digit window. |
Motion blur & squish #
Digits blur and compress while rolling fast, then sharpen and spring back as they settle:
NumberFlow(
value: v,
motionBlur: 8, // max blur sigma (px) at peak speed
motionBlurVelocityScale: 6, // lower = blur ramps in sooner / stronger
motionBlurThreshold: 0.5, // roll speed below which no blur is applied
digitSquish: 0.3, // horizontal squish: scaleX → 1 - 0.3 at peak
)
The blur is a soft frosted blur over the whole digit that ramps up while it rolls fast and fades to fully crisp as it settles — so only the moving digits are blurred, never the settled ones.
digitSquish compresses a rolling digit horizontally (to 1 - digitSquish) at
peak speed and springs it back to full width as it settles, for a snappy,
motion-compressed feel. It only affects the digits that are actually moving.
With NumberFlowSpring.ios, motion blur, digit squish, and stagger all default
to iOS-preset values automatically — a prominent blur (peak sigma 4, velocity
scale 4), a 0.3 squish, and a left-to-right stagger. Pass explicit values to
override any of them.
Per-digit constraints #
Cap individual wheels (e.g. a rating that only goes 0–5 in the ones place):
NumberFlow(
value: rating,
// position 0 = ones, 1 = tens, …
digits: {0: const DigitConstraint(max: 5)},
)
Scrub mode #
For a value that updates faster than the widget can rebuild (a slider drag, a
chart scrubber), drive it from a ValueListenable<num>. The widget re-formats
each tick and rolls only the changed digits, repainting without a full rebuild
unless the digit count changes:
final scrub = ValueNotifier<num>(0);
NumberFlow.scrub(
value: scrub,
format: const NumberFlowFormat.currency(currencyCode: 'USD', symbol: r'$'),
tabularNums: true, // steady width for smooth scrubbing
)
// later, from a gesture:
scrub.value = newValue;
TimeFlow #
An animated clock, countdown, or stopwatch. It's prop-driven — the parent owns
the ticking (a Timer, Ticker, or stream) and pushes new values.
// Explicit fields
TimeFlow(hours: 12, minutes: 30, seconds: 45)
// From a Duration (countdown / stopwatch)
TimeFlow.duration(
elapsed,
showHours: elapsed.inHours > 0,
showCentiseconds: true,
)
// From a DateTime (wall clock)
TimeFlow.dateTime(DateTime.now(), showSeconds: true, is24Hour: false)
TimeFlow accepts the same animation properties as NumberFlow
(spinTiming, spring, motionBlur, wheelSpacing, mask, …).
Callbacks #
Both widgets accept onAnimationsStart and onAnimationsFinish:
NumberFlow(
value: v,
onAnimationsStart: () => debugPrint('rolling'),
onAnimationsFinish: () => debugPrint('settled'),
)
Accessibility #
Each widget exposes a plain-text semantics label of the formatted value (e.g.
"$1,234.56", or "1.2E4" for scientific notation), so screen readers announce
the number rather than individual rolling glyphs. When the platform reduce-motion
setting is on, transitions snap instead of rolling (unless
respectMotionPreference: false).
Example #
See the example/ app for an interactive gallery: counter,
currency, percent, compact, scientific, a live stopwatch, a scrub slider, and
sliders for every animation parameter.
License #
See the LICENSE file in the repository.