Digits count up from zero to the target number. Type a number as the sample text to try it.
export function counterRoll(el) {
const _reducedMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches
const _duration = _reducedMotion ? 0.01 : 1.2
const _delay = _reducedMotion ? 0 : 0
const raw = el.textContent ?? ''
const match = raw.match(/[\d,]*\d/)
if (!match) {
return () => {}
}
const idx = match.index ?? 0
const prefix = raw.slice(0, idx)
const suffix = raw.slice(idx + match[0].length)
const target = Number(match[0].replace(/,/g, ''))
const points = [0, 0.075, 0.1461, 0.2135, 0.2772, 0.3374, 0.3942, 0.4476, 0.4978, 0.5448, 0.5889, 0.6299, 0.6682, 0.7037, 0.7366, 0.767, 0.7949, 0.8205, 0.8439, 0.8651, 0.8844, 0.9017, 0.9172, 0.9309, 0.9431, 0.9537, 0.963, 0.9709, 0.9776, 0.9831, 0.9877, 0.9914, 0.9942, 0.9964, 0.9979, 0.9989, 0.9995, 0.9999, 1, 1]
const durationMs = _duration * 1000
const delayMs = _delay * 1000
let raf = 0
let start = null
let cancelled = false
const tick = (now) => {
if (cancelled) return
if (start === null) start = now
const elapsed = now - start - delayMs
if (elapsed < 0) {
raf = requestAnimationFrame(tick)
return
}
const t = Math.min(1, elapsed / durationMs)
const value = target * easeAt(points, t)
el.textContent = prefix + Math.round(value).toLocaleString() + suffix
if (t < 1) {
raf = requestAnimationFrame(tick)
} else {
}
}
raf = requestAnimationFrame(tick)
return () => {
cancelled = true
cancelAnimationFrame(raf)
el.textContent = raw
}
}
function easeAt(points, t) {
const clamped = t < 0 ? 0 : t > 1 ? 1 : t
const scaled = clamped * (points.length - 1)
const i = Math.floor(scaled)
const frac = scaled - i
const a = points[i]
const b = points[Math.min(i + 1, points.length - 1)]
return a + (b - a) * frac
}