← Easeout LabFree component
Number ticker
Counts up once when it scrolls into view, writing to the DOM so React never re-renders per frame.
74
Needspnpm add motion
"use client"
import { useEffect, useRef, useState } from "react"
import { animate, useInView, useReducedMotion } from "motion/react"
// Counts up from 0 the first time it scrolls into view, and again on Replay. The number is written straight
// to the DOM, so React doesn't re-render on every frame. Screen readers get the final value only.
export function NumberTicker({ value = 74 }: { value?: number }) {
const ref = useRef<HTMLSpanElement>(null)
const inView = useInView(ref, { once: true, amount: 0.6 })
const reduce = useReducedMotion()
const [run, setRun] = useState(0)
useEffect(() => {
const el = ref.current
if (!el || !inView) return
if (reduce) {
el.textContent = String(value)
return
}
const controls = animate(0, value, {
duration: 1.2,
ease: [0.16, 1, 0.3, 1],
onUpdate: (v) => (el.textContent = String(Math.round(v))),
})
return () => controls.stop()
}, [inView, reduce, value, run])
return (
<div className="flex flex-col items-center gap-4">
<p className="m-0 text-[64px] leading-none font-semibold tracking-[-0.03em] tabular-nums">
<span ref={ref} aria-hidden="true">
0
</span>
<span className="sr-only">{value}</span>
</p>
{!reduce && (
<button
type="button"
onClick={() => setRun((n) => n + 1)}
className="h-11 cursor-pointer rounded-full border border-current/20 bg-transparent px-5 text-[14px] font-medium text-current focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-current"
>
Replay
</button>
)}
</div>
)
}