Signature
MotionNumber
Animated numeric readout — a smooth count-up, or an odometer that rolls each digit reel into place (reduced-motion aware).
● LIVE
Throughput
847req/s
Signal
99.9%
Credits
12,480
"use client";import { MotionNumber } from "@/registry/okibi/ui/motion-number";export default function MotionNumberDemo() { return ( <div className="flex flex-wrap items-end justify-center gap-8"> <div> <div className="font-ui text-xs uppercase tracking-[0.12em] text-muted-foreground"> Throughput </div> <div className="font-display text-4xl font-extrabold leading-none sm:text-5xl"> <MotionNumber value={847} /> <span className="ml-1 font-mono text-xs uppercase tracking-[0.1em] text-muted-foreground"> req/s </span> </div> </div> <div> <div className="font-ui text-xs uppercase tracking-[0.12em] text-muted-foreground"> Signal </div> <div className="font-display text-4xl font-extrabold leading-none sm:text-5xl text-signal-spark"> <MotionNumber value={99.9} decimals={1} /> <span className="ml-1 font-mono text-xs uppercase tracking-[0.1em] text-muted-foreground"> % </span> </div> </div> <div> <div className="font-ui text-xs uppercase tracking-[0.12em] text-muted-foreground"> Credits </div> <div className="font-display text-4xl font-extrabold leading-none sm:text-5xl"> <MotionNumber value={12480} duration={1.2} format={(n) => Math.round(n).toLocaleString("en-US")} /> </div> </div> </div> );}npx shadcn@latest add https://okibi.cndr.dev/r/motion-number.jsonInstallation
Install the theme first, then add the component:
npx shadcn@latest add https://okibi.cndr.dev/r/motion-number.jsonInstall the required dependencies:
npm install motionCopy the source from the Code tab above into components/ui/motion-number.tsx. It uses the cn helper and the okibi theme tokens — install the theme first if you haven't.
Usage
import { MotionNumber } from "@/components/ui/motion-number"
<MotionNumber value={1024} variant="odometer" digits={4} />Examples
Formatted
Roll to fixed decimals, or pass a format function for units, percentages, and currency.
● LIVE
decimals
99.94
format
87%
integer
847
"use client";import { MotionNumber } from "@/registry/okibi/ui/motion-number";export default function FormatDemo() { return ( <div className="flex flex-wrap items-end justify-center gap-8"> <div> <div className="font-mono text-xs uppercase tracking-grid-wide text-muted-foreground"> decimals </div> <div className="font-display text-4xl font-extrabold leading-none"> <MotionNumber value={99.94} decimals={2} /> </div> </div> <div> <div className="font-mono text-xs uppercase tracking-grid-wide text-muted-foreground"> format </div> <div className="font-display text-4xl font-extrabold leading-none text-signal-spark"> <MotionNumber value={87} format={(n) => `${n.toFixed(0)}%`} /> </div> </div> <div> <div className="font-mono text-xs uppercase tracking-grid-wide text-muted-foreground"> integer </div> <div className="font-display text-4xl font-extrabold leading-none"> <MotionNumber value={847} /> </div> </div> </div> );}Odometer
Set variant="odometer" to roll each digit on its own vertical 0–9 reel into place with a hard settle, cascading right-to-left — mechanical digit travel rather than the default smooth value interpolation. Pad to a fixed width with digits.
● LIVE
01280
"use client";import * as React from "react";import { MotionNumber } from "@/registry/okibi/ui/motion-number";export default function MotionNumberOdometerDemo() { const [value, setValue] = React.useState(1280); React.useEffect(() => { const id = setInterval( () => setValue(Math.round(1000 + Math.random() * 9000)), 2200 ); return () => clearInterval(id); }, []); return ( <MotionNumber variant="odometer" value={value} digits={5} className="font-display text-5xl font-bold text-signal-spark" /> );}API Reference
MotionNumber
| Prop | Type | Default | Description |
|---|---|---|---|
value | number | – | Target value to animate to. |
variant | "count" | "odometer" | "count" | Interpolate the text, or roll digit reels. |
duration | number | 0.9 | Roll duration in seconds (count). |
digits | number | – | Minimum zero-padded integer digits (odometer). |
decimals | number | – | Fixed decimal places. |
format | (n: number) => string | – | Custom formatter; overrides decimals (count). |
Accessibility
- Both variants expose the resolved final value to assistive tech via an
sr-onlyaria-live='polite'node, so screen readers read the settled number, not the interpolation. - The animating count text and every odometer digit reel are decorative and marked
aria-hidden. - Honors
prefers-reduced-motion: the final value renders instantly with no count-up or reel roll in either variant. - Renders
tabular-numsso the readout never shifts width as digits change. - For values that update on a fast interval, reconsider the polite live region so a screen reader is not flooded with announcements.
Guidelines
Do
- Use
countfor smooth metric count-ups andodometerfor a mechanical split-flap roll. - Set
digitsonodometerfor a fixed-width, zero-padded readout. - Drive color from the consumer with classes like
text-signal-spark; the component ships no color of its own.
Don't
- Do not point a polite
MotionNumberat a value that ticks many times a second without dampening the live region. - Do not expect
digitsto zero-pad thecountvariant — padding applies toodometeronly. - Do not wrap it in your own
aria-liveregion; it already exposes the resolved value to screen readers.