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

Installation

  1. Install the dependencies

    $ npm i motion lucide-react
  2. Copy the source into components/ui/stats-section.tsx

    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>
      );
    }
    
  3. Use it

    The file exports the component and a Demo showing typical usage. Import whichever you need.