Odometer
Every digit spins on its own wheel and lands on its value, like a car’s mileage counter. Commas, currency signs and units stay put.
$12,480
How to use it
- Paste the snippet just before
</head>. - Put the number in an element with
data-odometer. It rolls when it scrolls into view.
Options
Add any of these attributes to the same element.
data-values="A|B|C" | Several values to roll through in turn. |
data-duration | How long the wheels spin. Default 1800 ms. |
data-stagger | Delay between neighbouring digits. Default 90 ms. |
data-hold | How long each value stays, with data-values. Default 1800 ms. |
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/bundles/anime.umd.min.js"></script>
<script>
(function () {
// Odometer: every digit of a number spins on its own wheel and lands on its
// value, like a car's mileage counter. Commas, symbols and units stay still.
// Usage: <span data-odometer>12,480</span>
// Optional: data-values="1,250|8,420|23,914" (rolls through them in turn),
// data-duration (ms), data-stagger (ms between digits), data-hold (ms per value).
var DEFAULTS = { duration: 1800, stagger: 90, hold: 1800 };
var css =
'.od{display:inline-flex;font-variant-numeric:tabular-nums}' +
'.od-col{display:inline-grid;position:relative;clip-path:inset(0 -0.1em)}' +
'.od-size{visibility:hidden}' +
'.od-strip{position:absolute;left:0;right:0;top:0;display:flex;flex-direction:column;text-align:center;will-change:transform}' +
'.od-sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}';
var style = document.createElement('style');
style.textContent = css;
document.head.appendChild(style);
function setup(el) {
var list = el.dataset.values ? el.dataset.values.split('|') : [el.textContent];
var values = list.map(function (v) { return v.trim(); }).filter(Boolean);
el.classList.add('od');
el.textContent = '';
var sr = document.createElement('span');
sr.className = 'od-sr';
el.appendChild(sr);
var box = document.createElement('span');
box.setAttribute('aria-hidden', 'true');
box.style.display = 'inline-flex';
el.appendChild(box);
return { el: el, values: values, sr: sr, box: box, digits: [] };
}
// Lay out the value: a spinning column per digit, plain text for the rest.
function layout(o, value) {
o.sr.textContent = value;
o.box.textContent = '';
o.digits = [];
Array.from(value).forEach(function (ch) {
if (!/[0-9]/.test(ch)) {
var s = document.createElement('span');
s.textContent = ch;
o.box.appendChild(s);
return;
}
var col = document.createElement('span');
col.className = 'od-col';
var size = document.createElement('span');
size.className = 'od-size';
size.textContent = '0';
var strip = document.createElement('span');
strip.className = 'od-strip';
for (var i = 0; i < 20; i++) {
var n = document.createElement('span');
n.textContent = i % 10;
strip.appendChild(n);
}
col.appendChild(size);
col.appendChild(strip);
o.box.appendChild(col);
o.digits.push({ col: col, strip: strip, target: +ch });
});
}
function roll(o, value, onDone) {
var d = o.el.dataset;
var previous = o.digits.map(function (g) { return g.target; });
layout(o, value);
var h = o.digits.length ? o.digits[0].col.getBoundingClientRect().height : 0;
var count = o.digits.length;
o.digits.forEach(function (g, i) {
g.strip.childNodes.forEach(function (n) { n.style.height = h + 'px'; n.style.lineHeight = h + 'px'; });
// Start where this wheel was last time (aligned from the right), then spin
// forward at least one full turn to the new digit.
var from = previous[previous.length - (count - i)];
from = from === undefined ? 0 : from;
g.strip.style.transform = 'translateY(' + -from * h + 'px)';
anime.animate(g.strip, {
y: [-from * h, -(10 + g.target) * h],
duration: (+d.duration || DEFAULTS.duration) + i * 120,
delay: i * (+d.stagger || DEFAULTS.stagger),
ease: 'out(4)',
onComplete: i === count - 1 ? onDone : undefined
});
});
if (!count && onDone) onDone();
}
function run(o) {
var d = o.el.dataset;
var i = 0;
(function next() {
roll(o, o.values[i], function () {
if (o.values.length < 2) return;
i = (i + 1) % o.values.length;
setTimeout(next, +d.hold || DEFAULTS.hold);
});
})();
}
function init() {
var reduced = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (e) {
if (!e.isIntersecting) return;
io.unobserve(e.target);
run(e.target._od);
});
}, { threshold: 0.3 });
document.querySelectorAll('[data-odometer]').forEach(function (el) {
if (el._od) return;
var o = (el._od = setup(el));
if (reduced) { o.box.textContent = o.values[0]; o.sr.textContent = o.values[0]; return; }
layout(o, o.values[0].replace(/[0-9]/g, '0'));
io.observe(el);
});
}
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', init);
else init();
})();
</script><p>Trusted by <span data-odometer>12,480</span> teams</p>