Installation
Registry — recommended
Copies the component into your configured shadcn UI directory and installs Motion.
npx shadcn@latest add https://suluu.site/r/counter-numbers.jsonnpm package
For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.
pnpm add suluu motionUsage
The rolling number is display-only: pass it the latest value and keep buttons, timers, and server updates in your application. A changed place rolls once instead of counting through every value, so large jumps remain quick and legible.
"use client"
import { useState } from "react"
import { CounterNumbers } from "@/components/ui/counter-numbers"
export function DownloadCount() {
const [downloads, setDownloads] = useState(1284)
return (
<p>
<CounterNumbers aria-live="polite" value={downloads} /> downloads
<button onClick={() => setDownloads((value) => value + 1)}>
Add download
</button>
</p>
)
}// app/globals.css
@import "suluu/styles.css";
@source "../node_modules/suluu/dist";
// component.tsx
import { CounterNumbers } from "suluu/counter-numbers"
export function Revenue() {
return <CounterNumbers className="text-4xl font-semibold" value={1284} />
}Locales and number styles
Formatting is delegated to Intl.NumberFormat. Digits keep identities based on their numeric place while currency signs, grouping separators, compact suffixes, and other symbols resize around them.
<CounterNumbers
locales="de-DE"
formatOptions={{
currency: "EUR",
style: "currency",
}}
value={1234.56}
/>
<CounterNumbers
formatOptions={{ maximumFractionDigits: 1, notation: "compact" }}
motionIntensity="expressive"
value={1284500}
/>In context
Rolling only the changed places gives a compact stats row a sense of continuity between refreshes.
Release overview
Last 30 days
- Readers
- 7,420
- Saves
- 1,284
- Shares
- 392
Props
CounterNumbers accepts native span attributes and forwards its ref to the root span. It deliberately owns its children so the visual and accessible values cannot drift apart.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | number | — | Numeric value to format and display. |
| locales | Intl.LocalesArgument | "en-US" | Locale or preference list passed to Intl.NumberFormat. |
| formatOptions | Intl.NumberFormatOptions | — | Grouping, fraction, currency, unit, percent, compact, or sign options. |
| motionIntensity | "subtle" | "default" | "expressive" | "default" | Travel, stagger, and spring character of changed digit places. |
| aria-live | "off" | "polite" | "assertive" | — | Opts meaningful value changes into screen-reader announcements. |
| className | string | — | Typography and layout classes on the root. |
Theming
The component has no color, surface, or size preset and declares no CSS variables. It inherits the surrounding font, color, line height, and OpenType numeral support; use className just as you would on a normal inline span. Tabular numerals are requested by default to keep columns steady.
Accessibility
Assistive technology receives one complete formatted value, never the temporary stack of entering and exiting glyphs. Announcements are intentionally opt-in: add aria-live="polite" only when the update is meaningful, not for decorative counters or high-frequency telemetry.
Under prefers-reduced-motion, the latest formatted value replaces the previous one immediately without rolling, fading, staggering, or retaining exit layers. The preference is observed live if it changes while the page is open.