Skip to Content

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.

1,234
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.

$19.99/mo
<NumberFlow.Root value={value} format={{ style: "currency", currency: "USD" }} suffix="/mo" />

Compact notation

Use notation: "compact" for human-readable large numbers (1.2K, 89M).

1.2K
<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.

20
<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):

120120
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.

Scroll the panel to trigger the animation.
↓ Keep scrolling ↓
0
↑ Scroll back up ↑
// 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.

$124.23+5.64%
<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.

PropTypeDefaultDescription
valuenumberrequiredThe number to display. Transitions whenever this changes.
formatIntl.NumberFormatOptionsFormatting options forwarded to Intl.NumberFormat. Covers currency, percent, compact notation, fraction digits, sign display, etc.
localesIntl.LocalesArgumentruntime defaultBCP-47 locale tag (or list) used for formatting.
prefixstringStatic text rendered before the number.
suffixstringStatic text rendered after the number.
trendnumber | ((oldValue: number, value: number) => number)(old, v) => Math.sign(v - old)Spin direction. +1 always up, -1 always down, 0 shortest path per digit.
isolatebooleanfalseIsolate this instance’s transitions from sibling layout updates. Ignored inside a NumberFlow.Group.
animatedbooleantrueSet to false to finish any current animation and disable further ones — useful while a value is being edited live.
transformTimingEffectTiminglibrary defaultTiming for layout-related transforms.
spinTimingEffectTimingfalls back to transformTimingTiming for the digit spin animations.
opacityTimingEffectTiminglibrary defaultTiming for character fade-in / fade-out.
digitsRecord<number, { max?: number }>Per-position digit configuration (e.g. cap a digit at 5 for the tens place of a clock). Not reactive — set once.
respectMotionPreferencebooleantrueHonor prefers-reduced-motion: reduce.
pluginsPlugin[][continuous]Plugins from @number-flow/react. continuous is applied by default; pass [] to opt out or your own list to override.
animateInViewboolean | AnimateInViewOptionsfalseWhen enabled, displays 0 until the element scrolls into view, then animates to value. See Animate when scrolled into view.
willChangebooleanfalseApply will-change hints to internals. Useful for high-frequency updates; costs memory.
noncestringCSP nonce forwarded to NumberFlow’s inline <style> blocks during SSR/hydration.
onAnimationsStart(e: CustomEvent<undefined>) => voidFires when an update animation begins. Distinct from the DOM onAnimationStart.
onAnimationsFinish(e: CustomEvent<undefined>) => voidFires when an update animation completes.
classNamestringMerged via cn onto the underlying custom element.
...restReact.HTMLAttributes<NumberFlowElement>Forwarded to the underlying custom element.

Data attributes: data-slot="number-flow-root", plus data-in-view ("true" | "false") when animateInView is enabled.

AnimateInViewOptions

OptionTypeDefaultDescription
enabledbooleanToggles the behavior. Required when passing the options form.
oncebooleantrueDisconnect after the first intersection. Set false to re-animate every time it re-enters.
onIntersect(entry: IntersectionObserverEntry) => voidFires on each intersection. Stable across renders — inline callbacks are fine.
rootElement | Document | nullviewportIntersectionObserver root. Use to scope to a scroll container.
rootMarginstring"0px"IntersectionObserver rootMargin.
thresholdnumber | number[]0IntersectionObserver threshold.

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:

PropertyDefaultEffect
--number-flow-mask-height.25emHeight of the gradient mask at the top and bottom of the number. Also used as vertical padding.
--number-flow-mask-width.5emWidth of the gradient mask at the left and right of the number.

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? })true when 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()true when the browser supports the underlying custom element features, regardless of motion preference.
  • usePrefersReducedMotion()true when 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.Root will not scale smoothly during transitions. For animated chrome, wrap the number in a motion container and animate that container’s layout instead.

Exported types

import type { AnimateInViewOptions, NumberFlowGroupProps, NumberFlowRootProps, } from "@togetheragency/ui/number-flow";
Last updated on