Rolling Number
An animated, odometer-style number counter. Give it a value and it rolls there — inside any element: text, buttons, table cells, inputs, textareas and SVG.
Continuous odometer
Counts through the values in between; fast digits spin as reels, 9 → 0 always rolls forward, and a new value mid-animation continues from what is on screen.
Any element
Digit columns inside containers; animated text inside inputs, textareas, options and SVG text. Your other children are never touched.
Real number formatting
Currencies, percent, units, locales and native digits through Intl, plus 99+ badges, prefixes and suffixes.
Events & promises
rn-start / rn-end events everywhere, and setValue() resolves when the animation ends.
Accessible
An accessible name with the real value, optional live announcements, and instant changes under reduced motion.
Typed, SSR-ready, styles included
Typed options in every IDE, server-rendered fallbacks, and a stylesheet injected automatically (CSP-friendly).
Installation & quick start
Install the package; the stylesheet is injected automatically the first time a counter is created.
Quick start
Live playground
Change a setting and the counter updates right away. The code below is generated from your settings, ready to copy.
Works in any element
The same counter attaches to the element you already have. Containers get animated digit columns; form fields, <option> and SVG text are animated as text, so the value stays a normal value.
Inline text<span>
You have new messages
Button badge<button>
Table cells<td>
| Revenue | |
| Growth |
Input<input readonly>
Textarea<textarea>
Select option<option>
SVG text<text>
When visibletrigger
Number formatting
Setting locale or format switches on Intl: separators, currency symbols, units and native digits. The digits still roll; the symbols stay in place.
Events & promises
Every change fires rn-start and rn-end on the host (they bubble), and setValue() returns a promise that resolves when the animation ends.
const counter = document.querySelector('rolling-number');
counter.addEventListener('rn-start', (e) => console.log('start', e.detail)); // { value, from, animated }
counter.addEventListener('rn-end', (e) => console.log('end', e.detail.value));
const { completed } = await counter.setValue(500); // true when it reached 500
counter.setValue(10, false); // roll each digit
counter.setValue(10, { motion: 'none' }); // instant
counter.setValue(10, { duration: 1500, easing: 'ease-out-expo' });
// React: onRnStart / onRnEnd · Vue: @rn-start / @rn-end · Angular: (rnStart) / (rnEnd)
CSS variables lab
The counter inherits your font, size and color. These custom properties tune the animation and the look; set them on the counter or on any ancestor.
| Property | Default | Effect |
|---|---|---|
--rn-duration | 0.5s | Duration of rolls; a non-default value also times counting (when the JS duration is unset). |
--rn-easing | cubic-bezier(0.2, 1, 0.3, 1) | Easing of rolls; a non-default value also eases counting. |
--rn-lh | 1 | Height of the digit window (and line height). Raise it if tall glyphs are clipped. |
--rn-fill | — | Background image painted into the glyphs (pair with color: transparent). |
--rn-bleed | 0px | Lets glows and overhangs show outside the digit window (pair with a taller --rn-lh). |
--rn-decoration | — | Underline or strike-through that stays still while the digits roll. |
[data-rn-state="animating"] | — | Present on the host while it moves: style the running animation. |
Framework guides
Every integration takes the same options and fires the same rn-start / rn-end events.
Running live on this page
Options
Every option is optional and works the same way in every integration. Invalid input never throws: it falls back to the default.
| Option | Attribute | Default | What it does |
|---|---|---|---|
value | value | 0 | The number to show, rounded to the displayed precision. |
pad | pad | 1 | Minimum integer digits, padded with zeros (1–64). |
max | max | null | Display limit, e.g. 99 for a badge; the accessible label keeps the real value. |
overflowSuffix | overflow-suffix | '' | Shown after the digits above max: '+' → 99+. |
interpolate | interpolate, no-interpolate | true | true counts through the values; false rolls each digit straight to its new digit. |
duration | duration | auto | Animation length in ms (auto: 400 when counting, --rn-duration when rolling). |
easing | easing | auto | A preset (odometer, ease-out-expo, …), a CSS easing or a function. |
decimals | decimals | auto | Fixed fraction digits (0–20); auto = the currency's digits, else 0. |
locale | locale | — | BCP 47 locale, e.g. 'de-DE'; turns on Intl formatting. |
format | format (JSON) | — | Intl options: currency, percent, unit, signDisplay, useGrouping, numberingSystem. |
prefixText | prefix-text | '' | Static text before the number. |
suffixText | suffix-text | '' | Static text after the number. |
mode | mode | 'auto' | 'columns' or 'text'; auto picks text for form fields and SVG text. |
from | from | — | Value shown at creation, then the counter animates to value. |
trigger | trigger | 'immediate' | 'visible': animations wait until the counter is on screen. |
label | label | the value | Accessible name: '{value} points' or a function. |
live | live | 'off' | 'polite' / 'assertive': announce settled values. |
injectStyles | inject-styles | true | false turns off the automatic stylesheet for this counter. |
React and Vue: props. Angular: rollingNumber (the value), pad, max, interpolate, duration, and rn + the option name for the others (rnFormat, rnLocale, …). update(): undefined = unchanged, null = back to the default.
API & events
const core = new RollingNumberCore(element, options) — the element and every framework binding wrap this class.
| Method | Description |
|---|---|
setValue(value, animate?) | Sets the target; returns Promise<{ value, completed }>. animate: true (count), false (roll) or { motion, duration, easing }. |
update(options) | Changes several options (undefined = unchanged, null = default). |
whenSettled() | Promise for the current animation (resolved at once when idle). |
finish() | Jumps to the target now. |
getValue() / getDisplayValue() | The target / the value on screen. |
getText() | The formatted target text, e.g. "99+". |
isAnimating() | true while the display moves. |
destroy() | Removes only the counter's nodes and restores the element. |
| Event | When |
|---|---|
rn-start | The display starts moving (also instant changes, with animated: false). detail = { value, from, animated }. |
rn-end | The display settled on the final value; the DOM is final. |
| Function | Description |
|---|---|
rollingNumber(node, param) | Action for Svelte, Solid, Alpine, Lit: { update, destroy, core }. |
configureRollingNumber({ injectStyles, nonce }) | Global configuration (call before the first counter). |
injectRollingNumberStyles(root) | Adds the stylesheet to a document or shadow root yourself. |
renderStaticText() / renderStaticHTML() | Server-side rendering helpers (also in rolling-number/ssr). |
defineRollingNumber(tagName?) | Registers the element under another tag name. |
100 counters at once
All counters on a page share a single requestAnimationFrame loop that batches DOM reads and writes.