Number Ticker
Counts up to any number when it scrolls into view.
0+
Active teams
0.00%
Uptime
0.0
Average rating
components/ui/number-ticker.tsx
"use client";
import { animate, motion, useInView, useMotionValue, useTransform } from "motion/react";
import { useEffect, useRef } from "react";
type NumberTickerProps = {
value: number;
/** Animation length in seconds */
duration?: number;
decimals?: number;
prefix?: string;
suffix?: string;
className?: string;
};
export function NumberTicker({
value,
duration = 2,
decimals = 0,
prefix = "",
suffix = "",
className = "",
}: NumberTickerProps) {
const ref = useRef<HTMLSpanElement>(null);
const inView = useInView(ref, { once: true, amount: 0.6 });
const count = useMotionValue(0);
const display = useTransform(
count,
(v) =>
`${prefix}${v.toLocaleString("en-US", {
minimumFractionDigits: decimals,
maximumFractionDigits: decimals,
})}${suffix}`,
);
useEffect(() => {
if (!inView) return;
const controls = animate(count, value, { duration, ease: [0.16, 1, 0.3, 1] });
return () => controls.stop();
}, [inView, value, duration, count]);
return (
<motion.span ref={ref} className={`tabular-nums ${className}`}>
{display}
</motion.span>
);
}
export default function Demo() {
const stats = [
{ value: 12480, label: "Active teams", suffix: "+" },
{ value: 99.99, label: "Uptime", suffix: "%", decimals: 2 },
{ value: 4.9, label: "Average rating", decimals: 1 },
];
return (
<div className="grid w-full max-w-2xl grid-cols-1 gap-8 text-center sm:grid-cols-3">
{stats.map((s) => (
<div key={s.label}>
<NumberTicker
value={s.value}
suffix={s.suffix}
decimals={s.decimals}
className="text-4xl font-semibold tracking-tight text-zinc-900"
/>
<p className="mt-1 text-sm text-zinc-500">{s.label}</p>
</div>
))}
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/number-ticker.tsxcomponents/ui/number-ticker.tsx"use client"; import { animate, motion, useInView, useMotionValue, useTransform } from "motion/react"; import { useEffect, useRef } from "react"; type NumberTickerProps = { value: number; /** Animation length in seconds */ duration?: number; decimals?: number; prefix?: string; suffix?: string; className?: string; }; export function NumberTicker({ value, duration = 2, decimals = 0, prefix = "", suffix = "", className = "", }: NumberTickerProps) { const ref = useRef<HTMLSpanElement>(null); const inView = useInView(ref, { once: true, amount: 0.6 }); const count = useMotionValue(0); const display = useTransform( count, (v) => `${prefix}${v.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals, })}${suffix}`, ); useEffect(() => { if (!inView) return; const controls = animate(count, value, { duration, ease: [0.16, 1, 0.3, 1] }); return () => controls.stop(); }, [inView, value, duration, count]); return ( <motion.span ref={ref} className={`tabular-nums ${className}`}> {display} </motion.span> ); } export default function Demo() { const stats = [ { value: 12480, label: "Active teams", suffix: "+" }, { value: 99.99, label: "Uptime", suffix: "%", decimals: 2 }, { value: 4.9, label: "Average rating", decimals: 1 }, ]; return ( <div className="grid w-full max-w-2xl grid-cols-1 gap-8 text-center sm:grid-cols-3"> {stats.map((s) => ( <div key={s.label}> <NumberTicker value={s.value} suffix={s.suffix} decimals={s.decimals} className="text-4xl font-semibold tracking-tight text-zinc-900" /> <p className="mt-1 text-sm text-zinc-500">{s.label}</p> </div> ))} </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.