Odometer
Rolling-drum counter that carries like real gearing. Lit, backlit and cream drums, locale-aware separators.
Installation
npx shadcn@latest add https://neonkit.heyaadi.com/r/odometer.jsonUsage
import { Odometer } from "@/components/ui/odometer"
<Odometer value={48213} />Examples
Lit, backlit and cream
Lit glows the digits on dark drums. Backlit prints dark ink on drums lit from behind. Cream is a car odometer, with the tenths drum in the neon color.
Carrying
A drum only turns while every drum to its right rolls from 9 to 0, so 999 to 1000 turns four drums at once. Leading drums stay dim until the count reaches them.
Small, with units
A suffix is printed on the faceplate. Color can follow the value.
API reference
| Prop | Type | Default |
|---|---|---|
value | numberThe reading. Negative values show as zero. | None |
from | numberStarts here and rolls to value once the counter scrolls into view. | None |
decimals | numberFixed decimal places, each on its own drum. | 0 |
minDigits | numberFewest whole-number drums. Unused ones show a dim zero. | 1 |
locale | stringSeparators and digits. Fixed by default so server and client render the same. | "en-US" |
grouping | booleanPrints group separators. | true |
prefix | ReactNodePrinted before the drums, like "$". | None |
suffix | ReactNodePrinted after the drums, like "km" or "ms". | None |
variant | "lit"Glowing digits, ink on a backlit drum, or ivory drums with black ink. | "lit" |
size | "sm"Window height of 28, 40 or 64px, or any pixel value. Also settable with a --odo-h class. | 40px |
duration | numberMilliseconds per roll. By default it grows with the distance, from about 0.5s to 2.4s. | None |
housing | booleanDraws the housing around the drums. | true |
color | NeonColorPalette name or hex. Falls back to --neon-accent, then cyan. | inherited |
Accessibility
- A polite live region holds the final formatted value, prefix and suffix included. It never reads the rolling digits.
- With reduced motion the drums jump straight to the new value.
- Drums are moved by one rAF loop that writes transforms directly, so a rolling counter never re-renders React.