NumberFlow
A thin, headless wrapper around @number-flow/react.
Renders a custom element that animates digit transitions whenever value
changes. Formatting is driven by standard Intl.NumberFormatOptions, so
currencies, percentages, compact notations, and locales work out of the box.
import { NumberFlow } from "@togetheragency/ui/number-flow";
export function Counter({ value }: { value: number }) {
return <NumberFlow.Root value={value} />;
}Import
import { NumberFlow } from "@togetheragency/ui/number-flow";The component is exported as a compound object:
NumberFlow.Root;
NumberFlow.Group;The animation hooks from the underlying library are re-exported as well:
import {
useCanAnimate,
useIsSupported,
usePrefersReducedMotion,
} from "@togetheragency/ui/number-flow";Composition
NumberFlow is a single primitive (Root) plus an optional grouping wrapper
(Group). Root renders a custom element that lays out and animates its
digits internally — there is no separate trigger or content slot.
<NumberFlow.Group>
<NumberFlow.Root value={price} format={{ style: "currency", currency: "USD" }} />
<NumberFlow.Root value={delta} format={{ style: "percent" }} />
</NumberFlow.Group>NumberFlow.Root is a custom element
under the hood. Internal pieces are exposed via CSS shadow parts
(::part(number), ::part(digit), ::part(integer), etc.) so consumers
can target them without piercing the shadow tree.
Usage
Formatting with Intl.NumberFormat
format is forwarded to Intl.NumberFormat — anything the platform supports
works. Pair it with prefix / suffix for affixes that should sit outside
the animated number itself.
<NumberFlow.Root
value={value}
format={{ style: "currency", currency: "USD" }}
suffix="/mo"
/>Compact notation
Use notation: "compact" for human-readable large numbers (1.2K, 89M).
<NumberFlow.Root value={value} format={{ notation: "compact" }} />Trend
trend controls the spin direction. The default is Math.sign(value - oldValue),
so digits go up when the number grows and down when it shrinks. Passing 0
animates each digit toward the closest direction independently — useful when
you want movement without implying a positive or negative change.
<NumberFlow.Root value={value} trend={0} />Continuous plugin (default)
The continuous plugin is applied by default, so digit transitions pass
through every intermediate value out of the box — producing a slot-machine
feel. It has no effect when trend is 0.
To opt out, pass an empty plugins array. To extend, pass your own list
(re-import continuous if you still want it):
import { continuous, NumberFlow } from "@togetheragency/ui/number-flow";
// Default — continuous is applied automatically.
<NumberFlow.Root value={value} />
// Opt out.
<NumberFlow.Root value={value} plugins={[]} />
// Explicit (equivalent to the default).
<NumberFlow.Root value={value} plugins={[continuous]} />Animate when scrolled into view
Set animateInView to defer the animation until the element enters the
viewport. The component renders 0 until it intersects, then animates to
value. Pass true for sensible defaults or an options object to customize
the IntersectionObserver and react to entries.
// Shorthand — defaults to `once: true`, observes the viewport.
<NumberFlow.Root value={1_234_567} animateInView />
// Full options.
<NumberFlow.Root
value={1_234_567}
animateInView={{
enabled: true,
once: true,
root: scrollContainer,
rootMargin: "0px",
threshold: 0.5,
onIntersect: (entry) => console.log("in view", entry),
}}
/>When enabled, NumberFlow.Root exposes the observed state via the
data-in-view attribute, which you can target from CSS.
Synchronized group
When one number’s layout affects another’s — stacked rows, a price plus a
delta percentage, a list of stats — wrap them in NumberFlow.Group so their
transitions stay in sync. The wrapper renders no DOM of its own.
<NumberFlow.Group>
<NumberFlow.Root
value={price}
format={{ style: "currency", currency: "USD" }}
/>
<NumberFlow.Root
value={delta}
format={{
style: "percent",
maximumFractionDigits: 2,
signDisplay: "exceptZero",
}}
/>
</NumberFlow.Group>Reduced motion
respectMotionPreference defaults to true. When a user has
prefers-reduced-motion: reduce set, NumberFlow finishes any in-flight
transitions and skips new ones. Pass respectMotionPreference={false} to
opt out and always animate.
<NumberFlow.Root value={value} respectMotionPreference={false} />API Reference
NumberFlow.Root
Renders the animated number. Forwards its ref to the underlying
NumberFlowElement custom element.
Data attributes: data-slot="number-flow-root", plus data-in-view
("true" | "false") when animateInView is enabled.
AnimateInViewOptions
NumberFlow.Group
Wraps multiple NumberFlow.Root instances so their transitions are
orchestrated together. Renders no DOM and accepts only children. Has no
props beyond children — useful whenever one animated number’s layout shift
would otherwise reflow another mid-animation.
<NumberFlow.Group>{/* NumberFlow.Root instances */}</NumberFlow.Group>Styling
NumberFlow exposes its internals through CSS shadow parts. Inspect the
rendered element in DevTools to see what’s available; the common ones are
number, digit, integer, fraction, prefix, and suffix.
number-flow-react::part(suffix) {
color: var(--muted-foreground);
margin-inline-start: 0.125em;
}There are also a couple of CSS custom properties:
font-variant-numeric: tabular-nums is recommended on the container or the
element itself to keep digit widths constant during transitions.
Hooks
import {
useCanAnimate,
useIsSupported,
usePrefersReducedMotion,
} from "@togetheragency/ui/number-flow";useCanAnimate({ respectMotionPreference? })—truewhen the browser supports the required APIs and (optionally) the user is okay with motion. Useful for conditionally rendering a<NumberFlow.Root>versus a plain formatted number.useIsSupported()—truewhen the browser supports the underlying custom element features, regardless of motion preference.usePrefersReducedMotion()—truewhen the user has requested reduced motion.
Limitations
Inherited from @number-flow/react:
- Scientific and engineering notations are not supported.
- Non-Latin digits and RTL locales are not currently supported.
- Backgrounds and borders on
NumberFlow.Rootwill not scale smoothly during transitions. For animated chrome, wrap the number in amotioncontainer and animate that container’s layout instead.
Exported types
import type {
AnimateInViewOptions,
NumberFlowGroupProps,
NumberFlowRootProps,
} from "@togetheragency/ui/number-flow";