Rolling stats
A row of key figures that roll as they come into view.
Documentation
-
Copy to Webflow
Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: Webflow removes any font your site has not installed. -
Add custom CSS
Custom CSS in WebflowThis CSS already ships inside the Webflow embed, so a pasted section needs nothing added. Paste it yourself, in the page or the site custom code, only if the effect runs on several pages. The tutorial explains where.
See the tutorialBefore the script runs, a marked number is hidden withvisibilityand keeps its room, so the written number never flashes before the drums. It shows by itself after four seconds if the script never comes, and at once without JavaScript, through the noscript line the paste carries inside its Embed. The size, weight and tracking of a number are the page's own; the stylesheet only asks for tabular figures, so nothing moves sideways. -
Add custom JS
Custom JavaScript in WebflowThis script already ships inside the Webflow embed, settings block included. You paste it yourself, before the closing body tag of the page or the site custom code, only when the effect has to run on several pages, and the tutorial says how to choose.
See the tutorial -
Use it on your own elements
How to add a custom attribute in WebflowA custom attribute is a pair, a name and a value, typed in the Settings panel of the element you selected, under Custom attributes. The name is what the script looks for, the value is what it reads. The tutorial shows the panel.
See the tutorialCustom attributesSelect the element on the canvas and open the Settings panel. Under Custom attributes, add a pair: the name on the left, the value on the right.Custom attribute What it does data-rolling-stats Marks the block that holds the figures: a row, a grid, a whole section. It takes no value: leave the value field empty. Every number marked data-rolling-stats-value inside it rolls as it comes into view. A single number takes both attributes on the same element. data-rolling-stats-value On each number. Left empty, the number is read from the text as it is written: every digit becomes a drum, the commas, spaces and points between digits stay in place, and what comes before the first digit or after the last one comes in once the number has landed. Given a bare number, data-rolling-stats-value="124800", that number is shown instead, its thousands joined by the separator setting. Without it on any element of the block, nothing rolls and the console says so. [ Required ] data-rolling-stats-rule On an empty box near a number, in the same item: the rule of that figure. Its own height and colour are the track, and the script lays the fill over it. Without it, the script adds a hairline under the number. [ Optional ] data-rolling-stats-prefix On a number: the words shown before it, data-rolling-stats-prefix="$", in place of what the text holds before its first digit. data-rolling-stats-suffix does the same for the words after it. Meant for a number given in data-rolling-stats-value. [ Optional ] data-rolling-stats-state Written by the engine on the block: ready while figures wait, playing, done once all have played, static under reduced motion. Each number carries data-rolling-stats-stat the same way. Never written by hand. [ Optional ] SettingsEvery setting is a custom attribute too, added the same way and on the same element. An attribute always wins over the defaults written in the code.Attribute Default What it is data-rolling-stats-color #ffab1a Colour of the rule while it fills, and of the flash a digit carries as its drum lands; once the number has landed the rule takes the ink of the text. Options: any CSS colour, the amber #ffab1a by default. data-rolling-stats-ink inherit Colour of the digits and of the words around them. Options: inherit (the colour of the text, the default), or any CSS colour. data-rolling-stats-duration 1.6 Time one figure takes, in seconds, from the first move to its last drum at rest. The drums to the left land earlier, 0.11 s apart. data-rolling-stats-stagger 0.12 Delay, in seconds, between two figures that come into view together, in the order of the markup. data-rolling-stats-overshoot 0.5 How far a drum runs past its digit before it is taken back, in digits, 0 to 2. data-rolling-stats-separator , Mark put between the thousands of a number given in data-rolling-stats-value. A number read from the text keeps the marks it was written with, and this is not used. A point here turns the decimal mark into a comma. Options: , by default, a point, a space, an apostrophe, or nothing for no grouping. data-rolling-stats-once true Whether a figure plays one time only. Options: true (played once, then nothing watches the page any more), false (back on its start digits once it has fully left the window, and played again as it returns). data-rolling-stats-start-from zero The digits the drums wait on before they roll. Options: zero (every drum on 0), random (each drum on a digit of its own, the same on every load).
Two files, no build step.
It runs in the browser only. Load it with a script tag in plain HTML, in a
client-side script in Astro, never in the frontmatter. In React, call
RollingStats.mount(ref.current) in useEffect and
destroy() in its cleanup.
-
Tune it, either way
Per element with an attribute, or once for the whole site by editingDEFAULTSat the top of the file.Attribute Default What it is Lower Higher data-rolling-stats-color #ffab1a Colour of the rule while it fills, and of the flash a digit carries as its drum lands; once the number has landed the rule takes the ink of the text. Options: any CSS colour, the amber #ffab1a by default. data-rolling-stats-ink inherit Colour of the digits and of the words around them. Options: inherit (the colour of the text, the default), or any CSS colour. data-rolling-stats-duration 1.6 Time one figure takes, in seconds, from the first move to its last drum at rest. The drums to the left land earlier, 0.11 s apart. A quick tick, the roll barely read A long, heavy roll; past 2.5 the last digits crawl data-rolling-stats-stagger 0.12 Delay, in seconds, between two figures that come into view together, in the order of the markup. The whole row leaves at once A wave that runs along the row data-rolling-stats-overshoot 0.5 How far a drum runs past its digit before it is taken back, in digits, 0 to 2. At 0 the drums ease onto their digit and stop, no click The next digit shows more of itself before the snap; at 1 it is in the window whole data-rolling-stats-separator , Mark put between the thousands of a number given in data-rolling-stats-value. A number read from the text keeps the marks it was written with, and this is not used. A point here turns the decimal mark into a comma. Options: , by default, a point, a space, an apostrophe, or nothing for no grouping. data-rolling-stats-once true Whether a figure plays one time only. Options: true (played once, then nothing watches the page any more), false (back on its start digits once it has fully left the window, and played again as it returns). data-rolling-stats-start-from zero The digits the drums wait on before they roll. Options: zero (every drum on 0), random (each drum on a digit of its own, the same on every load). -
Drive it from JavaScript
When figures arrive after the page has loaded, when they have to roll again from code, or when the page reacts to them landing.const element = document.querySelector('[data-rolling-stats]'); element.addEventListener('rolling-stats:done', (e) => { console.log(e.detail); }); const stats = RollingStats.mount(element); // the block auto() mounted, or a new one stats.replay(); // back to the start digits, then rolled again stats.show(); // the numbers at once, nothing rolled // Figures added after the page loaded: mount everything marked inside it. RollingStats.auto(container); // When the block leaves the page: stats.destroy(); // the numbers go back exactly as they were written
-
Fit it to your page
- Two attributes.
data-rolling-statson the block that holds the figures, a row, a grid, a whole section;data-rolling-stats-valueon each number. Neither takes a value. A single number takes both on the same element. In Webflow, select the element, open the Settings panel and add the custom attribute with an empty value. - The number is read from the text, as it is written. In
124,800+,98.6%,$4.2Mor1 250,5 €, every digit becomes a drum, whatever sits between two digits stays where it is, and what comes before the first digit or after the last one is kept as words, which come in once the number has landed. Nothing is formatted for you, so nothing is formatted against you: write the number the way the page's language writes it. - A number from an attribute, for a CMS field that holds a bare number:
data-rolling-stats-value="124800". Its thousands are joined byseparator, anddata-rolling-stats-prefixanddata-rolling-stats-suffixon the same element give the words around it. The text of the element is then what a browser without JavaScript shows. - The rule. Mark a box of the page with
data-rolling-stats-rule, near the number, in the same item: its own height and colour are the track, and the script lays the fill over it. With no marked box, the script adds a hairline under the number,rolling-stats-rule, which one class restyles or hides (display: none). - When it plays. A figure plays once 60% of it is inside the window, 8% up from the bottom edge. Figures that come in together leave
staggerapart; one that comes in later, in a column on a phone, plays when its own turn comes. - Settings on one block:
data-rolling-stats-<key>on it,data-rolling-stats-duration="2"ordata-rolling-stats-start-from="random". For every block of the page, without opening the file:window.RollingStatsSettings = { color: '#15100c' }in a script placed beforerolling-stats.js. The three levels, least specific first, areDEFAULTS, then that object, then the attribute. - Nothing moves sideways. The figure is set in tabular figures by the stylesheet, before and after the script, and a drum is exactly as wide as its digit. The words before and after the number keep their room from the start. The size, weight and tracking of the number are the page's own.
- In the Webflow Designer the numbers are plain. The canvas runs no Embed, so a marked number shows as written, editable; the drums only exist on the published page and in the preview.
- Screen readers read the number once, as written: the text stays in the element, visually hidden, and the drums are
aria-hidden. - Touch: the same, it plays on scroll. Nothing listens to the pointer.
- **
prefers-reduced-motion**: the numbers stand as written from the start, the rules full, the words in. Nothing rolls and nothing flashes. - States, for a style or a test:
data-rolling-stats-stateon the block isready(figures waiting),playing,done(all of them played) orstatic(reduced motion). Each change fires a bubbling event of the same name,rolling-stats:ready,rolling-stats:playing,rolling-stats:done,rolling-stats:static, whosedetailis{ element }. Each number carriesdata-rolling-stats-stat,ready,playing,doneorstill. - From JavaScript:
RollingStats.mount(el, overrides)returns the instance, which carrieselement,settings(the values it resolved),state(),isRunning(),play()(every figure now, wherever it is),show()(the numbers at once, nothing rolled),reset()(back on the start digits, waiting),replay()(reset, then played again for what is in view) anddestroy().RollingStats.auto(root)mounts every marked block insideroot,RollingStats.replay(root)plays them again,RollingStats.destroy(root)takes them all back.destroy()puts the original text nodes back and removes what the script added, so the markup is the one of snippet.html and mounting again starts clean. In React,mountinuseEffectanddestroyin its cleanup. - The words of the markup are the preview's, and they ship as they are: pasted, the page reads the way the preview did. Put the user's own figures in their place whenever they want.
- On Webflow, add the fonts first. Add Geist and Geist Mono to your site's fonts (Site settings, Fonts, Google Fonts) before pasting: the paste names them, and the Designer drops a font the site does not have.
- Two attributes.
-
Avoid the pitfalls
- A face without tabular figures. The stylesheet asks for
font-variant-numeric: tabular-nums; a typeface that has none keeps its proportional digits, and the number then breathes by a pixel or two as a 1 follows an 8. Most text faces have them, Geist does. - Old style figures. A drum is one line tall plus 0.12em above and below. Digits that hang far under the baseline, in a serif with old style figures, can be cut at the foot of the window; ask the face for lining figures (
font-variant-numeric: lining-nums tabular-nums). - The number does not wrap. The digits, their marks and the words around them are kept on one line. A long figure in a narrow column needs a smaller size, which is what the preview does under 767px.
- Text changed after mount is not read again. The number is read once, when the block is mounted. To change it,
destroy(), change the text,mount()again. - **Settings handed to
mount()after the page loaded are dropped.**auto()mounts on DOM ready, andmount()on a block already mounted returns the existing instance. A page-wide block goes inwindow.RollingStatsSettings, whichauto()reads itself. - Keep the noscript line. Before the script runs, a marked number is hidden and keeps its room, so the written number never flashes before the drums. Without JavaScript the noscript line is what shows it; if the script fails, the number shows by itself after four seconds.
- A block inside a closed tab or a hidden menu waits: it plays when it is shown and in the window, not before.
- **The colour of the flash is
color.** On a dark section giveinkor the text a light colour; the flash and the rule keepcolor, so pick one that reads on that ground.
- A face without tabular figures. The stylesheet asks for
One click on Copy the AI prompt copies everything an assistant needs to build this effect: the complete code, the markup, the dependency, every setting and every pitfall.
What it does
Rolling stats is a scroll animation for Webflow and vanilla JavaScript, with no library. Also called rolling number counter, odometer counter animation or number ticker animation.
A row of key figures that roll as they come into view. Every digit sits on a drum of its own. All the drums of a figure leave at the same moment, fast, and slow down to a crawl in which the last digits tick past at a pace the eye reads; the left drum lands first and the right one last, and each one runs half a digit past its mark and is taken back, the click of a mechanical counter. A landed digit flashes in the colour and takes the ink. The figures are tabular and the commas, spaces and points between digits never move, so nothing shifts sideways. Under each figure a rule fills while it rolls, and the words around the number, $, %, +, M, come in only once it has landed. Figures of one row leave one after the other. The number is read from the text as it is written, so there is nothing to format: any grouping, any language. Off screen nothing runs, and once played the component sleeps. One clock, no library. Under reduced motion the numbers stand as written with the rule full. Pattern type count up, odometer, number ticker.
Updated
Questions
Add the custom attribute data-rolling-stats to the block that holds the figures and data-rolling-stats-value to each number, both with an empty value. In the Designer a marked number shows as written and stays editable; the drums only exist in the preview and on the published page.
No. Each digit sits on a drum of its own and one requestAnimationFrame clock turns them all, with no library and no build step. Off screen nothing runs, and once a block has played nothing watches the page any more.
Write the number in the text the way it should read, 124,800+, 98.6% or $4.2M: every digit becomes a drum, the marks between digits stay where they are, and what sits before or after the number comes in once it has landed. For a CMS field that holds a bare number, write data-rolling-stats-value="124800"; its thousands are then joined by data-rolling-stats-separator, a comma by default.
Raise data-rolling-stats-duration, the time in seconds one figure takes from its first move to its last drum at rest, 1.6 by default. Lower is a quick tick that is barely read, and past 2.5 the last digits crawl. data-rolling-stats-stagger, 0.12 by default, is the delay between two figures that come into view together.
No, by default a figure plays one time. With data-rolling-stats-once="false" it goes back to its start digits once it has fully left the window and plays again as it returns. A figure starts once 60% of it is inside the window.