NumberFlow

 for 
431.10

An animated number component. Dependency-free. Accessible. Customizable.

Basic usage

<!-- Basic usage -->
<number-flow></number-flow>

<script type="module">
	import 'number-flow'
	// or, if not using a bundler:
	// import 'https://esm.sh/number-flow'

	const flow = document.querySelector('number-flow')
	// Sets the initial value:
	flow.update(123)
</script>

Subsequent calls to .update() will trigger animations.

Properties

format: Intl.NumberFormatOptions

Formatting options for the number.

flow.format = { notation: 'compact' }

locales: Intl.LocalesArgument

The locale(s) for the number.

numberPrefix: string, numberSuffix: string

A custom prefix or suffix for the number.

$3/moClick anywhere to change numbers
flow.format = { style: 'currency', currency: 'USD', trailingZeroDisplay: 'stripIfInteger' }
flow.numberSuffix = "/mo"

Timings

There are three properties to customize the animation timings. Each accept an EffectTiming object:

124.23Click anywhere to change numbers
// Used for layout-related transforms:
flow.transformTiming = {
	duration: 700, easing: 'linear(...)'
}
// Used for the digit spin animations.
// Will fall back to `transformTiming` if unset:
flow.spinTiming = {
	duration: 700, easing: 'linear(...)'
}
// Used for fading in/out characters:
flow.opacityTiming = {
	duration: 350, easing: 'ease-out'
}

For spring-based easings, I’d recommend Kevin Grajeda’s generator or easing.dev.

trend: number (oldValue: number, value: number) => number Default: (oldValue, value) => Math.sign(value - oldValue)

Controls the direction of the digits. If trend is or returns

  • +1: the digits always go up.
  • 0: each digit goes up if it increases and down if it decreases. This can be useful if you want to animate number changes without conveying an overall trend (example).
  • -1: The digits always go down.
20Click anywhere to change numbers

animated: booleanDefault: true

Can be set to false to disable all animations and finish any current ones.

digits: Record<number, { max?: number }>

Configure digits based on their position in the number (i.e. for 342.5, the positions are: 324120.5-1). This can be helpful for time-related displays, to ensure e.g. 59 -> 00. See the countdown example for a demo.

digits is not reactive to save on bundle size. If you need it to be reactive, please submit a feature request.

respectMotionPreference: booleanDefault: true

Can be set to false to animate regardless of the user’s reduced motion preference.

plugins: Plugin[]

Plugins to apply to the component. Currently there’s only one plugin, continuous, which makes the number transitions appear to pass through in-between numbers:

120
Click anywhere to change numbers

This plugin has no effect if trend is 0.


Attributes

data-will-change

If set, NumberFlow applies will-change properties to relevant elements. This can be useful if:

  • Your number is guaranteed to change frequently
  • You experience unwanted repositioning when a transition completes

Note that “excessive use of will-change will result in excessive memory use” (source: MDN).

<number-flow data-will-change></number-flow>

Events

animationsstart: (e: CustomEvent) => void

Triggered when update animations start. Not to be confused with the built-in animationstart event, which would trigger for animations on the <number-flow> element itself.

animationsfinish: (e: CustomEvent) => void

Triggered when update animations finish.


Styling

NumberFlow exposes parts for styling purposes:

$3/moClick anywhere to change numbers

You can use your browser’s inspector to see which part attributes are available to style. Note that changing the font-size of digits is difficult due to the CSS techniques NumberFlow uses.

::part styles may cause a flash of unstyled content in old browsers.

See workaround You can use feature detection to apply ::part styles only to browsers that support Declarative Shadow DOM (DSD). Add the following snippet to your <head>:

<script>
	if (
		HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode') ||
		HTMLTemplateElement.prototype.hasOwnProperty('shadowRoot') // old Chrome/Edge
	)
		document.documentElement.setAttribute('data-supports-dsd', '')
</script>

Then ensure your ::part styles use it:

If you’re using Tailwind, you can do this with a custom variant:

// tailwind.config.js
import plugin from 'tailwindcss/plugin'

export default {
	// ...
	plugins: [
		plugin(({ matchVariant }) => {
			matchVariant('part', (p) => `:root[data-supports-dsd] &::part(${p})`)
		})
	]
}

There’s also some CSS properties you can use to style the component:

--number-flow-mask-[height|width]: <length>Default: .25em | .5em

These adjust the height and width of the gradient fade-out masks at the edges of the number. --number-flow-mask-height also gets used as the top and bottom padding for the number.

--number-flow-char-height: <length>Default: 1em

Sets the height of each character. This can be used to adjust the spacing between digits during spin animations.

font-variant-numeric: tabular-nums

Ensures all numbers are the same width, which can prevent digits from shifting during transitions. See MDN for more information.


Grouping

If a <number-flow> affects another <number-flow>‘s position, you can wrap them in a <number-flow-group> to properly sync their transitions. Make sure to import 'number-flow/group':

Click anywhere to change numbers

<number-flow-group> syncs all descendant <number-flow>s, not just children.


Utilities

renderInnerHTML: (value: Value, props: Props) => string

Renders the inner HTML of a <number-flow> for SSR (server-side rendering). Here’s a usage example with Astro:

---
import { renderInnerHTML } from 'number-flow'
---
<number-flow
	set:html={renderInnerHTML(123, {
		// Make sure to pass in the `locales`, `format`,
		// `numberPrefix`, and `numberSuffix`
		// properties you're using:
		locales: 'en-US',
		format: { notation: 'compact' }
	})}
/>

<script>
	import 'number-flow'

	const flow = document.querySelector('number-flow')
	flow.locales = 'en-US'
	flow.format = { notation: 'compact' }
	// The first .update() call hydrates the component.
	// Make sure to pass the same value as you did in `renderInnerHTML()`:
	flow.update(123)
</script>

canAnimate: boolean

true if NumberFlow can animate, i.e. the browser supports the supports the required features.

import { canAnimate } from 'number-flow'

prefersReducedMotion: MediaQueryList

Whether the user prefers reduced motion.

import { prefersReducedMotion } from 'number-flow'

if (prefersReducedMotion.matches) {
	// User prefers reduced motion
}

Limitations