Step the number up and down. Wheels roll the way the value moves, only the digits that change turn, and 999 → 1,000 widens as the new digit and comma roll in. Click fast: they redirect mid-roll.
@keyframeui
registry/demos/odometer-demo.tsx
"use client";
import * as React from "react";
import { Odometer } from "@/components/ui/odometer";
const MAX = 9_999_999;
const steps = [
{ label: "−100", name: "Subtract 100", apply: (n: number) => n - 100 },
{ label: "−1", name: "Subtract 1", apply: (n: number) => n - 1 },
{ label: "+1", name: "Add 1", apply: (n: number) => n + 1 },
{ label: "+100", name: "Add 100", apply: (n: number) => n + 100 },
{ label: "×10", name: "Multiply by 10", apply: (n: number) => n * 10 },
];
const button =
"h-9 min-w-12 rounded-xl px-3 font-mono text-sm text-zinc-600 tabular-nums transition-[background-color,color,scale] duration-150 outline-none select-none hover:bg-zinc-100 hover:text-zinc-950 focus-visible:ring-2 focus-visible:ring-zinc-400 active:scale-[0.94] dark:text-zinc-400 dark:hover:bg-zinc-800 dark:hover:text-zinc-50 dark:focus-visible:ring-zinc-600";
// Click fast: every wheel turns from wherever it is.
export function OdometerDemo() {
const [value, setValue] = React.useState(1284);
const set = (next: number) => setValue(Math.min(MAX, Math.max(0, Math.round(next))));
const random = () => {
// A random length too, so the commas come and go.
const digits = 1 + Math.floor(Math.random() * 7);
set(Math.random() * 10 ** digits);
};
return (
<div className="flex w-full max-w-xl flex-col items-center">
<p className="text-[clamp(3.5rem,14vw,6.5rem)] leading-none font-semibold tracking-[-0.045em] text-zinc-950 dark:text-zinc-50">
<Odometer value={value} />
</p>
<div className="mt-12 flex flex-wrap items-center justify-center gap-0.5 rounded-2xl border border-zinc-200 bg-white p-1 shadow-sm shadow-zinc-950/5 dark:border-zinc-800 dark:bg-zinc-900 dark:shadow-black/40">
{steps.map((step) => (
<button
key={step.label}
type="button"
aria-label={step.name}
onClick={() => set(step.apply(value))}
className={button}
>
{step.label}
</button>
))}
<span aria-hidden className="mx-1 h-5 w-px bg-zinc-200 dark:bg-zinc-800" />
<button type="button" onClick={random} className={button}>
Random
</button>
</div>
</div>
);
}
Live preview, examples and props: https://keyframeui.com/components/odometer#odometer-demo























