
Rolling Number is an animated counter library that creates smooth, interruptible number and text transitions for modern web apps.
It renders dynamic values using vertical-rolling reels or mechanical split-flap cards for financial tickers, live dashboard metrics, and arrival-board headings.
Features
- Interruptible digit rolls that continue from the current animation state.
- Native locale formatting through
Intl.NumberFormat. numberandbigintvalues.- Rolling reel and split-flap animation modes.
- Direct text transitions and character-wheel animations through
RollingText. - Configurable duration, direction, stagger order, motion blur, and offscreen behavior.
- Bounded reel travel for large numeric changes.
- DOM controller methods for updates, remeasurement, immediate settlement, and cleanup.
- React and Solid adapters with readable server-rendered text.
- Reduced-motion handling and static localized fallback for formats that cannot roll correctly.
See it in Action
How To Use It
Installation
Install the rolling-number package.
npm install @kitlangton/rolling-number
bun add @kitlangton/rolling-number
Import the stylesheet with the DOM, React, or Solid entry point.
import '@kitlangton/rolling-number/styles.css';
Basic Usage
Create an element for the formatted value and initialize it through createRollingNumber().
<span id="balance"></span> <button id="update-balance">Update balance</button>
import { createRollingNumber } from '@kitlangton/rolling-number';
import '@kitlangton/rolling-number/styles.css';
const counter = createRollingNumber(
document.querySelector('#balance'),
{
value: 1234.56,
locales: 'en-US',
format: {
style: 'currency',
currency: 'USD'
}
}
);
document.querySelector('#update-balance').addEventListener('click', () => {
counter.update({ value: 1389.42 });
});
Browser ESM Setup
For browser environments using native ES modules without a bundler, you can load the distributed ESM file and stylesheet from a CDN.
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@kitlangton/rolling-number/dist/styles.css"
>
<span id="sales"></span>
<script type="module">
import { createRollingNumber }
from 'https://cdn.jsdelivr.net/npm/@kitlangton/rolling-number/dist/index.js';
const sales = createRollingNumber(
document.querySelector('#sales'),
{
value: 28450,
locales: 'en-US'
}
);
sales.update({ value: 29175 });
</script>
Currency, Percentages, And Other Number Formats
The format option accepts native Intl.NumberFormatOptions.
const revenue = createRollingNumber(
document.querySelector('#revenue'),
{
value: 8472.35,
locales: 'en-US',
format: {
style: 'currency',
currency: 'USD'
}
}
);
const conversionRate = createRollingNumber(
document.querySelector('#conversion-rate'),
{
value: 0.184,
locales: 'en-US',
format: {
style: 'percent',
maximumFractionDigits: 1
}
}
);
Split-Flap Numbers
Set mode to "flap" for hinged split-flap cards. flipDuration controls each card change. Flap faces need an opaque background.
const score = createRollingNumber(
document.querySelector('#score'),
{
value: 42,
mode: 'flap',
flipDuration: 180,
stagger: 'start'
}
);
#score {
--rn-flap-background: #111;
--rn-crease: 2px;
}
Animate Text
createRollingText() animates strings through character positions. The default "wheel" transition advances characters through FLAP_CHARSET. Characters outside the active wheel crossfade.
import { createRollingText } from '@kitlangton/rolling-number';
import '@kitlangton/rolling-number/styles.css';
const board = createRollingText(
document.querySelector('#status'),
{
text: 'BOARDING',
mode: 'flap',
stagger: 'start'
}
);
board.update({ text: 'DEPARTED' });
For headings, labels, emoji, lowercase text, or strings that should move directly from one glyph to its replacement, set transition to "direct".
const status = createRollingText(
document.querySelector('#order-status'),
{
text: 'Processing',
transition: 'direct',
stagger: 'start',
motionBlur: true
}
);
status.update({ text: 'Shipped 🚚' });
Rolling Number Options
RollingNumberOptions has three number-specific fields plus the shared motion options.
value(number | bigint, required): Numeric value to format and display.locales(string | string[], default: browser locale): Locale or locale list passed toIntl.NumberFormat.format(Intl.NumberFormatOptions, default:{}): Native number-formatting options.
Shared Motion Options
duration(number, default:500): Animation duration in milliseconds. Accepts0through10000.0disables motion.flipDuration(number, default: automatic): Duration of each hinged card in flap mode. Accepts1through10000milliseconds.animated(boolean, default:true):falsesettles updates immediately.motionBlur(boolean, default:false): Applies vertical blur to moving reels and turning flap halves.direction("auto" | "up" | "down", default:"auto"): Controls reel direction. Automatic number direction follows displayed magnitude. Text wheels advance forward automatically.pauseOffscreen(boolean, default:true): Offscreen instances retain their newest value and skip the roll.stagger("outward" | "start" | "end" | "none", default:"outward"):"outward"cascades new glyph positions from retained digits."start"and"end"sweep from either edge and also apply to changed wheels that stay in place."none"removes stagger delays.mode("roll" | "flap", default:"roll"): Chooses rolling reels or hinged split-flap cards.
Rolling Text Options
text(string, required): String displayed by the instance.transition("wheel" | "direct", default:"wheel"):"wheel"advances through configured character wheels."direct"rolls each glyph directly to its replacement.charset(string | readonly string[], default:FLAP_CHARSET): Defines wheel characters and their order. An array assigns a different wheel to each character position. Later positions reuse the final array entry when the array is shorter than the text. An empty array falls back toFLAP_CHARSET.
The default wheel is:
ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789:.-/&+'
A charset array can define specialized drums for clocks, codes, scores, or mixed letter-and-number boards.
const time = createRollingText(
document.querySelector('#time'),
{
text: '12:45',
mode: 'flap',
charset: [
'012',
'0123456789',
':',
'012345',
'0123456789'
]
}
);
Controller Methods
// Change one or more current options.
counter.update({ value: 1500 });
// Remeasure after typography, theme, or variable-font changes.
counter.refresh();
// Stop motion and display the newest value immediately.
counter.finish();
// Release resources and leave the final formatted text in the host.
counter.destroy();
DOM API
createRollingNumber(host, options): Creates a rolling numeric value and returns aRollingNumberController.createRollingText(host, options): Creates rolling or split-flap text and returns aRollingTextController.formatValue(value, options?): Formats anumberorbigintthrough the package’sIntl.NumberFormatlayer.FLAP_CHARSET: Default character wheel used byRollingText.
Styling And Customization
Style & customize the component using the following CSS custom properties:
--rn-blur(default:1): Scales vertical motion blur.0removes the smear.--rn-mask(default: built-in vertical gradient): Controls the reel mask. Setnonefor a hard clip.--rn-edge-fade(default:0.12em): Sets the fade depth at the top and bottom of a rolling reel.--rn-crease(default:1px): Sets the visible hinge gap between split-flap halves.--rn-flap-background(default: systemCanvascolor): Sets the opaque split-flap face background.
.metric {
--rn-edge-fade: 0.18em;
--rn-blur: 1.25;
}
.metric--sharp {
--rn-mask: none;
}
.departure-board {
--rn-flap-background: #181818;
--rn-crease: 2px;
}
Style Numbers By Trend
Hosts receive data-rn-trend="up", "down", or "none" when the displayed value changes. Wheel slots use data-rn-wheel, and flap wheel slots also use data-rn-flap.
.balance[data-rn-trend="up"] {
color: #138a45;
}
.balance[data-rn-trend="down"] {
color: #c73232;
}
React Usages
React 18 and 19 work with the /react entry. React is an optional peer dependency, and the entry keeps its use client boundary.
RollingNumber and RollingText accept their engine options plus standard span attributes, including className, style, aria-label, and refs. They exclude children and dangerouslySetInnerHTML.
Server rendering produces readable text. Initial hydration does not animate. Keep the initial value, locale, and formatting options identical on the server and client. Different ICU or CLDR data can produce different formatted strings, and hydration warnings are not suppressed.
import { RollingNumber } from '@kitlangton/rolling-number/react';
import '@kitlangton/rolling-number/styles.css';
export function Price() {
return (
<RollingNumber
value={1234.56}
locales="en-US"
format={{
style: 'currency',
currency: 'USD'
}}
duration={500}
/>
);
}
Solid Usages
Solid 1.9 or newer works with the /solid entry and remains an optional peer dependency.
RollingNumber and RollingText accept their engine options plus standard Solid span attributes. class and ref work on the outer span. The prop types exclude children, innerHTML, textContent, and innerText.
Server rendering keeps readable text. Hydration adopts the initial value without a first-load roll. Reactive prop changes pass to the DOM controller, and component cleanup destroys that controller.
import { createSignal } from 'solid-js';
import { RollingNumber } from '@kitlangton/rolling-number/solid';
import '@kitlangton/rolling-number/styles.css';
function Balance() {
const [value, setValue] = createSignal(1234.56);
return (
<RollingNumber
value={value()}
locales="en-US"
format={{
style: 'currency',
currency: 'USD'
}}
/>
);
}







