Skip to content
Suluu

CounterNumbers

A numeric display where only the places that changed move. Increases roll upward, decreases roll downward, carries retain stable columns, and the new glyphs settle with just enough spring to make frequent updates feel tangible.

Preview

Increase or decrease the value
1,284

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.json

npm package

For centralized upgrades, install the optional ESM package and configure Tailwind source detection as shown below.

pnpm add suluu motion

Usage

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.

Registry
"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>
  )
}
npm
// 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.

Formatting
<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
Metrics updated: 7420 readers, 1284 saves, and 392 shares.

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.

PropTypeDefaultDescription
valuenumberNumeric value to format and display.
localesIntl.LocalesArgument"en-US"Locale or preference list passed to Intl.NumberFormat.
formatOptionsIntl.NumberFormatOptionsGrouping, 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.
classNamestringTypography 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.

↑↓ to navigate↵ to openesc to close