Documentation
Everything you need to use RollingNumber, from installation to the full prop reference.
Getting started
Introduction
svelte-rolling-number provides a single Svelte 5 component, RollingNumber. It renders a line of text and, whenever its value changes, animates
each glyph that changed: departing glyphs roll out, scale down, blur and fade; arriving glyphs do
the reverse. Unchanged glyphs slide into their new positions.
The effect is inspired by SwiftUI's ContentTransition.numericText(value:)(opens in a new tab), brought to the web. The implementation is based on the web renderer from expo-content-transition(opens in a new tab), with a Svelte-native API.
Installation
Install the package from npm(opens in a new tab) with your package manager
of choice. It requires Svelte ^5.7.0.
npm install svelte-rolling-number Quick start
Import the component and pass it a value. Every other prop is optional.
<script lang="ts">
import { RollingNumber } from "svelte-rolling-number";
let count = $state(0);
</script>
<RollingNumber value={count} style="font-size: 48px" monospacedDigits />
<button onclick={() => count++}>Increment</button>The first value renders without animation. Every later change to value triggers a
transition.
Guides
Formatting values
RollingNumber does not format numbers itself. It animates whatever string it receives,
so format with Intl.NumberFormat, toFixed or anything else before
passing the value in. Passing a number is the same as passing String(value).
Glyphs are aligned around the decimal separator, so a change in the fractional part never shifts
the whole part. For locales that use a comma, set decimalSeparator:
<script lang="ts">
const euro = new Intl.NumberFormat("de-DE", {
style: "currency",
currency: "EUR",
});
let amount = $state(1299.5);
</script>
<!-- "1.299,50 €": align around the comma, not the dot. -->
<RollingNumber value={euro.format(amount)} decimalSeparator="," monospacedDigits />Add monospacedDigits whenever the value is a number. Without it, a narrow 1 turning into a wide 8 pushes every following glyph sideways.
Animating text
value can be any string, not just a number. Words, labels and emoji animate glyph
by glyph, the same way digits do:
<!-- Both labels start with "Sav", so only the ending rolls. -->
<RollingNumber value={saving ? "Saving…" : "Saved"} direction="up" />How old and new glyphs are paired depends on whether the value contains a digit:
- Plain text is matched from the start. A glyph that stays the same at the same
position doesn't move, so
Save → Savedonly rolls in thed. - Text around a number is anchored to the number: text before it is matched
from the start, text after it from the end.
3 items → 12 itemskeepsitemsin place while the count changes.
Matching is by position, not by searching for the text the two values share. Adding or removing
glyphs anywhere except the end shifts everything after them, so all of those glyphs roll: Saved → Unsaved animates every glyph. Text works best when the values share their
start, like a label that moves through stages.
Each grapheme cluster is one glyph. An emoji, a flag or a letter with combining accents rolls as a single piece, even when it is made of several code points.
Text has no numeric order, so direction="auto" can't tell which way to roll and
keeps the direction of the last change. Pin it to "up" or "down". monospacedDigits only affects digits, so it has no effect on letters.
Direction
With the default direction="auto", the component parses the old and new values as
numbers and rolls glyphs up when the value grows and down when it shrinks.
Content that does not read as a number, such as a clock, a date or a status label, should pin
the direction with "up" or "down". So should a countdown that must
always tick one way:
<script lang="ts">
import { RollingNumber } from "svelte-rolling-number";
let seconds = $state(10);
$effect(() => {
const id = setInterval(() => {
seconds = seconds === 0 ? 10 : seconds - 1;
}, 1000);
return () => clearInterval(id);
});
</script>
<RollingNumber
value={seconds}
direction="down"
bounce={0}
travel={1}
enterScale={1}
style="font-size: 56px; font-weight: 700"
monospacedDigits
/>Tuning the motion
These props shape the transition. They combine freely:
durationscales every spring, keeping the feel of the motion intact.bouncecontrols overshoot of the roll. Opacity and position never bounce.travelsets how far glyphs roll, relative to the line height.enterScalesets the size glyphs arrive at and leave with.blurIntensityandmaxBlurRadiuscontrol the motion blur.
<RollingNumber
value={value}
duration={700}
bounce={0.7}
travel={0.6}
enterScale={0.2}
blurIntensity={3}
maxBlurRadius={12}
/>Blur is the most expensive part of a transition. When many glyphs change at once, or when you
raise blurIntensity on large text, set maxBlurRadius to cap the cost.
The playground on the home page lets you try every value live.
Typography
There are no typography props. Like any inline text, the component inherits font, size, weight, style, letter spacing, colour and font features from the surrounding CSS, and glyphs are measured in exactly that typography.
<p class="distance">
<RollingNumber value="42,195 km" />
</p>
<style>
.distance {
font: italic 700 32px Georgia, serif;
letter-spacing: -0.5px;
color: rebeccapurple;
}
</style>To style one instance directly, pass class or style; both land on the
host element. When the inherited typography changes later, from a theme switch, a media query
or a web font finishing loading, glyphs are re-measured and laid out again without animating.
Disabling animation
Set animated={false} to apply changes instantly. The prop is reactive, so you can
turn animation on only when it is meaningful, for example after the first user interaction:
<!-- Only animate after the user interacts. -->
<RollingNumber value={total} animated={hasInteracted} />Accessibility
- The current value is real text inside the host, transparent behind the animated glyphs. Screen readers read it once, in line with the surrounding content, instead of glyph by glyph, and find in page, translation and copy see the value too.
- When the user has
prefers-reduced-motionenabled, animation is turned off regardless of theanimatedprop. - Value changes are not announced automatically. If an update matters to the user, pass
role="status"to make the component a polite live region, or wrap it in an element witharia-live="polite".
Server rendering
During server rendering and before hydration, the component renders the value as plain text in the inherited typography. When the glyph layer mounts, that copy stays in the layout, hidden, so the host keeps its size and text baseline when it sits inline. There is no layout shift and no empty box.
API reference
Content props
value string | numberThe text to display. Required. Measuring, diffing and animating all happen per glyph.
alignment "start" | "center" | "end"Horizontal alignment of the line inside the component's bounds.
Default: "start"
decimalSeparator stringThe character separating the whole and fractional parts. Glyphs are aligned around it, so 123.45 → 123.46 only animates the final digit. Only the first character is used.
Default: "."
monospacedDigits booleanAdds font-variant-numeric: tabular-nums so unchanged digits don't shift sideways when a neighbour changes. Unset, font-variant-numeric is inherited.
Default: false
Transition props
direction "auto" | "up" | "down"Which way glyphs travel. auto rolls up when the numeric value grows and down when it shrinks.
Default: "auto"
duration numberNominal transition duration in milliseconds. Scales every internal spring, so the character of the motion is preserved.
Default: 420
bounce numberHow far the vertical roll overshoots before settling. Clamped to 0–0.95; 0 arrives dead straight.
Default: 0.46
enterScale numberSize a glyph starts at when arriving and shrinks to when leaving. Clamped to 0–2; 1 disables scaling.
Default: 0.4
travel numberHow far a glyph rolls, as a fraction of the line height. Clamped to 0–3; 0 leaves only scale and fade.
Default: 0.333
blurIntensity numberBlurs each glyph in proportion to how far through its transition it is, multiplying radii derived from the line height. Clamped to 0–8; 0 turns blur off.
Default: 1
maxBlurRadius numberCeiling on the blur radius in CSS pixels. Caps the cost of a transition when many glyphs change at once.
Default: unbounded
clip booleanClips each glyph to the measured line box with a feathered edge, so rolling glyphs don't overlap surrounding content.
Default: true
animated booleanSet to false to apply values instantly. Also treated as false while prefers-reduced-motion is enabled.
Default: true
HTML attributes
Every other attribute is forwarded to the host <span>, including class, style, id, data-* and event handlers. role and children are reserved by the component, and aria-label is always set to the current value.
<RollingNumber
value={score}
class="score"
style="vertical-align: middle"
data-testid="score"
/>The host is an inline-block with an explicit width and height that follow the
content, so it can sit inside running text or a flex layout.
Types
All prop types are exported for use in wrapper components:
import type {
RollingNumberProps,
RollingNumberTransitionProps,
RollingNumberAlignment, // "start" | "center" | "end"
RollingNumberDirection, // "auto" | "up" | "down"
} from "svelte-rolling-number";