Number Ticker
Counts up to a value with an ease-out curve when it scrolls into view.
textdata
0
contributions
0
repositories
0
companies
components/sidd-ui/number-ticker.tsx
"use client"
import { useEffect, useRef, useState } from "react"
export function NumberTicker({
value,
duration = 1200,
locale = "en-US",
className,
}: {
value: number
duration?: number
locale?: string
className?: string
}) {
const ref = useRef<HTMLSpanElement>(null)
const [display, setDisplay] = useState(0)
useEffect(() => {
const el = ref.current
if (!el) return
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
setDisplay(value)
return
}
let frame = 0
const observer = new IntersectionObserver(([entry]) => {
if (!entry?.isIntersecting) return
observer.disconnect()
const start = performance.now()
const tick = (now: number) => {
const t = Math.min(1, (now - start) / duration)
const eased = 1 - Math.pow(1 - t, 3)
setDisplay(Math.round(eased * value))
if (t < 1) frame = requestAnimationFrame(tick)
}
frame = requestAnimationFrame(tick)
})
observer.observe(el)
return () => {
observer.disconnect()
cancelAnimationFrame(frame)
}
}, [value, duration])
return (
<span ref={ref} className={className} style={{ fontVariantNumeric: "tabular-nums" }}>
{display.toLocaleString(locale)}
</span>
)
}
Features
- Starts only when the number scrolls into view.
- Ease-out cubic curve, so it lands softly.
- Tabular digits so the width does not jump.
- Shows the final value immediately with reduced motion.
Why I built this
Numbers like a year of contributions mean more when you see them add up. A small count-up draws the eye without being loud.
Usage
example.tsx
<NumberTicker value={1938} />