Animated Number

A figure that rolls when it changes, one character at a time: each changed digit lifts out while its replacement rises from below. Backed by the motion tokens and tabular-nums, so the width never jumps.

2,450

Installation#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Add this component (its dependencies come along)npx koalaui-cli@latest add animated-number

Usage#

usage.tsxTSX
import { AnimatedNumber } from "@/components/ui/animated-number"export function Example({ total }: { total: string }) {  // Whenever `total` changes, the old value rolls up and the new rolls in.  return <AnimatedNumber value={total} className="text-2xl font-semibold" />}

Currency & formatting#

value takes whatever you want to display, a pre-formatted string is easiest. The container is tabular-nums, so even as the digit count changes the figure keeps a steady width and the layout never shifts. Only the digits are tabular: a comma, a period or a currency mark keeps its natural width, since a separator handed a digit’s advance leaves a hole either side of itself and sets 1,368 as 1 , 368 in a wide face. Format with Intl.NumberFormat / toFixed and pass the result.

$34.42

How it works#

The value is split into per-character cells, paired against the previous value from the right (the way a number grows). Only the characters that actually changed roll: each keeps two layers, the incoming glyph in normal flow and the outgoing one taken out of flow, so the cell never widens as the old glyph leaves. Everything else holds perfectly still at full strength, which is what keeps a change from reading as the whole figure dimming.

The changed cells fire in sequence, left to right, one --number-roll-stagger step (45ms) apart, so a figure sweeps across like a departure board. The stagger counts only the cells that move, so a change in the last two digits starts immediately, and it compresses past six steps so a long value still lands as one gesture. The two number-roll-* keyframes run at --duration-base with the opacity front-loaded against the travel (solid by 45%), so what you track is movement at full strength rather than a long half-visible cross-fade. There is no clip window, so no hard top or bottom edges. The first paint never animates, a screen reader gets the value as one uninterrupted string, and everything holds still under prefers-reduced-motion (the value simply swaps).

API reference#

AnimatedNumber

Renders one inline <span> (the per-character cells inside are managed for you). Props: value (the figure to show, any ReactNode, usually a formatted string; a change triggers the roll), className (styles the figure, e.g. size / weight / color), plus all <span> props.

FAQ#

No. The enter roll is gated so the initial paint shows the value at rest; only subsequent changes roll. This keeps a list of numbers from all rolling in at once on page load.