Odometer

A React number counter component whose digits roll like an odometer. For stats, dashboards and pricing.

Odometer playground

12,480
A rise rolls the digits up, a fall rolls them down. Fire again mid-roll - every column carries on from where it is.

Install Odometer

1. New project? One command installs the package and wires everything - see Installation:

bash
pnpm dlx zyncat-ui init

2. Import the component - it loads its own stylesheet:

tsx
import { Odometer } from '@zyncat/ui/odometer';

Usage

tsx
import { Odometer } from '@zyncat/ui/odometer';
 
<Odometer value={count} format={(v) => v.toLocaleString('en-US')} />

Compose Odometer in your application. No Tailwind or external styling library is required; every value resolves from Zyncat UI's token vocabulary.

Odometer props

Odometer

PropTypeDefaultDescription
...htmlAttributesHTMLAttributes—Standard <span> attributes (aria-*, data-*, title, ...) forwarded to the root.
value*number—The number to roll to. Digits roll up when it rises and down when it falls, and the odometer counts from zero the first time it is scrolled into view.
format(value: number) => stringString(Math.trunc(value))Renders the value into the displayed string; digits become rolling columns, everything else becomes a static separator.
speednumber1Multiplies the simulation rate; sampled live on every frame.
classNamestring—Extra class(es) merged onto the root.
styleOdometerStyle—Inline styles merged onto the root.

OdometerStyle

PropTypeDefaultDescription
--odometer-gapstring | number—`--odometer-gap` - Space between digit columns. Default: `0.02em`.
--odometer-inkstring | number—`--odometer-ink` - Digit ink. Default: `var(--text-strong)`.
--odometer-separator-inkstring | number—`--odometer-separator-ink` - The thousands separator. Default: `var(--text-muted)`.
--odometer-sizestring | number—`--odometer-size` - Digit size. Default: `var(--size-display)`.
--odometer-weightstring | number—`--odometer-weight` - Digit weight. Default: `var(--weight-medium)`.

Frequently asked questions

Import Odometer from '@zyncat/ui/odometer' and give it a value: <Odometer value={12480} />. Every time value changes the digit columns roll to the new number, each on its own spring, so there is no animation code to write. Under prefers-reduced-motion it snaps straight to the value instead of rolling.

Pass a format function. format={(v) => v.toLocaleString('en-US')} renders 12,480 with its comma, and format={(v) => String(v).padStart(8, '0')} renders 00012480. Digits in the returned string become rolling columns; everything else - commas, currency symbols, decimal points, units - stays put as a static separator.

Odometer has no scroll-trigger prop: render it with a starting value and set the real value when your own IntersectionObserver fires. The simulation already pauses whenever the element is off-screen or the tab is hidden, so a counter further down the page burns no frames before anyone sees it.

It is a real animation, not a GIF, a Lottie file or an After Effects template. Odometer renders live DOM - one column per digit, each driven by a spring simulation - so it counts to whatever value your app produces at runtime and stays sharp at any font size, which a pre-rendered number counter animation cannot do.

Yes. @zyncat/ui/odometer ships compiled ESM with its 'use client' directive intact, so it works in the Next.js App Router with no transpilePackages config. The formatted value is in the server-rendered HTML for crawlers and screen readers, and the digit columns start rolling once the component hydrates.

react-countup tweens one value from a start to an end over a duration you set. Odometer instead gives every digit column its own spring, so digits land out of sync and blur in proportion to their own velocity, and a value change mid-roll is absorbed rather than restarted. It adds no runtime dependencies - the motion is a small built-in engine, not GSAP or Framer Motion.

Set in Geist & Newsreader — animated by the house engineZyncat UI · Rev 0.11 · MIT · Built by Tabsir Ahammed · Source on GitHub