Stats Section
Bordered metrics band with numbers that count up in view.
0k+
Developers building
$0.0B
Revenue processed
0+
Countries served
0.0%
Guaranteed uptime
components/ui/stats-section.tsx
"use client";
import { animate, motion, useInView, useMotionValue, useTransform } from "motion/react";
import { useEffect, useRef } from "react";
type Stat = { value: number; suffix?: string; prefix?: string; label: string; decimals?: number };
function Counter({ value, suffix = "", prefix = "", decimals = 0 }: Omit<Stat, "label">) {
const ref = useRef<HTMLSpanElement>(null);
const inView = useInView(ref, { once: true });
const mv = useMotionValue(0);
const text = useTransform(mv, (v) => `${prefix}${v.toFixed(decimals)}${suffix}`);
useEffect(() => {
if (!inView) return;
const c = animate(mv, value, { duration: 2.2, ease: [0.16, 1, 0.3, 1] });
return () => c.stop();
}, [inView, mv, value]);
return <motion.span ref={ref}>{text}</motion.span>;
}
const defaultStats: Stat[] = [
{ value: 40, suffix: "k+", label: "Developers building" },
{ value: 2.4, suffix: "B", prefix: "$", decimals: 1, label: "Revenue processed" },
{ value: 180, suffix: "+", label: "Countries served" },
{ value: 99.9, suffix: "%", decimals: 1, label: "Guaranteed uptime" },
];
export function StatsSection({ stats = defaultStats }: { stats?: Stat[] }) {
return (
<div className="grid grid-cols-2 overflow-hidden rounded-3xl border border-zinc-200 bg-white lg:grid-cols-4">
{stats.map((s, i) => (
<motion.div
key={s.label}
initial={{ opacity: 0, y: 16 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true }}
transition={{ delay: i * 0.1, duration: 0.5 }}
className="border-zinc-200 p-8 text-center [&:not(:last-child)]:border-r max-lg:[&:nth-child(2)]:border-r-0 max-lg:[&:nth-child(-n+2)]:border-b"
>
<p className="text-4xl font-semibold tracking-tight text-zinc-900 tabular-nums">
<Counter {...s} />
</p>
<p className="mt-2 text-sm text-zinc-500">{s.label}</p>
</motion.div>
))}
</div>
);
}
export default function Demo() {
return (
<div className="w-full max-w-5xl">
<StatsSection />
</div>
);
}
Installation
Install the dependencies
$ npm i motion lucide-reactCopy the source into
components/ui/stats-section.tsxcomponents/ui/stats-section.tsx"use client"; import { animate, motion, useInView, useMotionValue, useTransform } from "motion/react"; import { useEffect, useRef } from "react"; type Stat = { value: number; suffix?: string; prefix?: string; label: string; decimals?: number }; function Counter({ value, suffix = "", prefix = "", decimals = 0 }: Omit<Stat, "label">) { const ref = useRef<HTMLSpanElement>(null); const inView = useInView(ref, { once: true }); const mv = useMotionValue(0); const text = useTransform(mv, (v) => `${prefix}${v.toFixed(decimals)}${suffix}`); useEffect(() => { if (!inView) return; const c = animate(mv, value, { duration: 2.2, ease: [0.16, 1, 0.3, 1] }); return () => c.stop(); }, [inView, mv, value]); return <motion.span ref={ref}>{text}</motion.span>; } const defaultStats: Stat[] = [ { value: 40, suffix: "k+", label: "Developers building" }, { value: 2.4, suffix: "B", prefix: "$", decimals: 1, label: "Revenue processed" }, { value: 180, suffix: "+", label: "Countries served" }, { value: 99.9, suffix: "%", decimals: 1, label: "Guaranteed uptime" }, ]; export function StatsSection({ stats = defaultStats }: { stats?: Stat[] }) { return ( <div className="grid grid-cols-2 overflow-hidden rounded-3xl border border-zinc-200 bg-white lg:grid-cols-4"> {stats.map((s, i) => ( <motion.div key={s.label} initial={{ opacity: 0, y: 16 }} whileInView={{ opacity: 1, y: 0 }} viewport={{ once: true }} transition={{ delay: i * 0.1, duration: 0.5 }} className="border-zinc-200 p-8 text-center [&:not(:last-child)]:border-r max-lg:[&:nth-child(2)]:border-r-0 max-lg:[&:nth-child(-n+2)]:border-b" > <p className="text-4xl font-semibold tracking-tight text-zinc-900 tabular-nums"> <Counter {...s} /> </p> <p className="mt-2 text-sm text-zinc-500">{s.label}</p> </motion.div> ))} </div> ); } export default function Demo() { return ( <div className="w-full max-w-5xl"> <StatsSection /> </div> ); }Use it
The file exports the component and a
Demoshowing typical usage. Import whichever you need.