All Taste components
Text effects
Number ticker
Stats that count up from zero and settle on the real number, which is also what shows at rest.
3 stats · suffixes · staggered
98%Say it saves time
64Countries
12kTeams shipping
"use client";
import { useEffect, useRef, useState } from "react";
interface NumberTickerProps {
value: number;
suffix?: string;
/** Milliseconds. */
duration?: number;
}
/** Renders the real value first (server, no JS, reduced motion), counts up once it scrolls into view. */
export function NumberTicker({ value, suffix = "", duration = 1800 }: NumberTickerProps) {
const [shown, setShown] = useState(value);
const ref = useRef<HTMLSpanElement>(null);
useEffect(() => {
const el = ref.current;
if (!el || window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
let raf = 0;
const io = new IntersectionObserver(([entry]) => {
if (!entry.isIntersecting) return;
io.disconnect();
const start = performance.now();
const tick = (now: number) => {
const p = Math.min(1, (now - start) / duration);
setShown(Math.round(value * (1 - Math.pow(1 - p, 4))));
if (p < 1) raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
});
io.observe(el);
return () => {
io.disconnect();
cancelAnimationFrame(raf);
};
}, [value, duration]);
return (
<span ref={ref} aria-label={value.toLocaleString() + suffix} className="tabular-nums">
<span aria-hidden>{shown.toLocaleString()}</span>
{suffix && <span aria-hidden className="text-primary">{suffix}</span>}
</span>
);
}
export function Stats() {
const stats = [
{ value: 98, suffix: "%", label: "Say it saves time" },
{ value: 64, suffix: "", label: "Countries" },
{ value: 12, suffix: "k", label: "Teams shipping" },
];
return (
<dl className="grid max-w-md grid-cols-3 divide-x divide-border">
{stats.map((s) => (
<div key={s.label} className="grid gap-0.5 px-3 first:pl-0">
<dd className="text-[34px] font-medium leading-tight tracking-tight text-fg">
<NumberTicker value={s.value} suffix={s.suffix} />
</dd>
<dt className="order-last text-[13px] text-fg-muted">{s.label}</dt>
</div>
))}
</dl>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.