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.
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
// 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
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.
animated: boolean
Default: 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
digits
is not reactive to save on bundle size.
If you need it to be reactive, please submit a
respectMotionPreference: boolean
Default: 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:
import { continuous } from 'number-flow'
flow.plugins = [continuous]
This plugin has no effect if trend
is 0
.
Attributes
data-will-change
If set, NumberFlow applies will-change
properties
- 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:
<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<number-flow>
element itself.
animationsfinish: (e: CustomEvent) => void
Triggered when update animations finish.
Styling
NumberFlow exposes
number-flow::part(suffix) {
margin-left: .0625em;
font-weight: normal;
font-size: 0.75em;
color: var(--muted);
}
You can use your browser’s inspector to see which part
attributesfont-size
of digits is difficult
due to the CSS techniques NumberFlow uses.
::part
styles may cause a flash of unstyled content in
See workaround
You can use feature detection
to apply ::part
styles only to browsers that support <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
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'
:
<number-flow-group
style="--number-flow-char-height: 0.85em"
class="flex items-center gap-4 font-semibold"
>
<number-flow id="number" class="~text-2xl/4xl"></number-flow>
<number-flow id="diff" class="~text-lg/2xl transition-colors duration-300"
></number-flow>
</number-flow-group>
<script type="module">
import 'number-flow'
import 'number-flow/group' // note the separate import
import { $number, $diff } from '../stores'
import { onReady } from '@/lib/dom'
onReady(() => {
const number = document.getElementById('number')
number.locales = 'en-US'
number.format = { style: 'currency', currency: 'USD' }
const diff = document.getElementById('diff')
diff.locales = 'en-US'
diff.format = { style: 'percent', maximumFractionDigits: 2, signDisplay: 'always' }
const unsubscribeNumber = $number.subscribe((value) => number.update(value))
const unsubscribeDiff = $diff.subscribe((value) => {
diff.classList.toggle('text-red-500', value < 0)
diff.classList.toggle('text-emerald-500', value >= 0)
diff.update(value)
})
return () => {
unsubscribeNumber()
unsubscribeDiff()
}
})
</script>
<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 ---
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
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
andScientific notations aren’t supported.engineering andNon-Latin digits aren’t currently supported.RTL locales - Backgrounds and borders on
<number-flow>
won’t scale smoothly during transitions.