# Stats Count Up Row — React Stats Section

> Four headline numbers in a divided row that count up from zero out of a blur when scrolled into view, each under a hairline that fills as it counts.

- Category: [Stats](https://pixelpapa.com/components/stats.md)
- Tags: stats, count-up, numbers, scroll, row
- Dependencies: motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/stats/stats-count-up-row

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/stats-count-up-row.json"
```

The file is written to `components/pixelpapa/stats-count-up-row.tsx`. To install manually, run `npm install motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { animate, motion, useInView, useReducedMotion } from "motion/react";
import { useEffect, useRef } from "react";

const ease = [0.22, 1, 0.36, 1] as const;

type Stat = { value: number; decimals?: number; prefix?: string; suffix?: string; label: string; note: string };

const defaultStats: Stat[] = [
  { value: 12400, suffix: "+", label: "Teams shipping with Kiln", note: "Across 61 countries" },
  { value: 99.99, decimals: 2, suffix: "%", label: "Uptime, trailing 12 months", note: "Publicly audited" },
  { value: 3.2, decimals: 1, suffix: "B", label: "Requests served each week", note: "Peak of 41k per second" },
  { value: 51, suffix: "s", label: "Median deploy time", note: "From push to live" },
];

export function CountUp({ value, decimals = 0, prefix = "", suffix = "", duration = 1.2 }: Omit<Stat, "label" | "note"> & { duration?: number }) {
  const ref = useRef<HTMLSpanElement>(null);
  const inView = useInView(ref, { once: true, amount: 0.6 });
  const reduce = useReducedMotion();

  useEffect(() => {
    const el = ref.current;
    if (!el || !inView) return;
    const fmt = (v: number) => v.toLocaleString("en-US", { minimumFractionDigits: decimals, maximumFractionDigits: decimals });
    const controls = animate(reduce ? value : 0, value, { duration, ease, onUpdate: (v) => (el.textContent = fmt(v)) });
    return () => controls.stop();
  }, [inView, reduce, value, decimals, duration]);

  return (
    <span className="tabular-nums">
      {prefix}
      <span ref={ref}>0</span>
      {suffix}
    </span>
  );
}

export default function StatsCountUpRow({ stats = defaultStats }: { stats?: Stat[] }) {
  return (
    <section className="w-full bg-background px-6 py-20 sm:py-28">
      <div className="mx-auto max-w-6xl">
        <div className="flex flex-col justify-between gap-4 sm:flex-row sm:items-end">
          <h2 className="max-w-lg text-3xl font-semibold tracking-tight text-balance text-foreground sm:text-4xl">
            Quietly running a meaningful slice of the web
          </h2>
          <p className="text-sm text-muted-foreground">Figures as of this quarter, refreshed monthly.</p>
        </div>

        <dl className="mt-14 grid gap-x-8 gap-y-12 sm:grid-cols-2 lg:grid-cols-4">
          {stats.map((s, i) => (
            <motion.div
              key={s.label}
              initial={{ opacity: 0, y: 20, filter: "blur(10px)" }}
              whileInView={{ opacity: 1, y: 0, filter: "blur(0px)" }}
              viewport={{ once: true, amount: 0.6 }}
              transition={{ duration: 0.8, delay: i * 0.1, ease }}
              className="relative pt-6"
            >
              <span aria-hidden className="absolute inset-x-0 top-0 h-px bg-border">
                <motion.span
                  initial={{ scaleX: 0 }}
                  whileInView={{ scaleX: 1 }}
                  viewport={{ once: true, amount: 0.6 }}
                  transition={{ duration: 1.2, delay: i * 0.1, ease }}
                  className="block size-full origin-left bg-primary"
                />
              </span>
              <dd className="text-5xl font-semibold tracking-tighter text-foreground sm:text-6xl">
                <CountUp {...s} />
              </dd>
              <dt className="mt-3 font-medium text-foreground">{s.label}</dt>
              <p className="mt-1 text-sm text-muted-foreground">{s.note}</p>
            </motion.div>
          ))}
        </dl>
      </div>
    </section>
  );
}
```

## AI prompt

```text
Add the "Stats Count Up Row" component from PixelPapa UI to this project.

What it is: Four headline numbers in a divided row that count up from zero out of a blur when scrolled into view, each under a hairline that fills as it counts.
Category: Stats. Depends on: motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/stats-count-up-row.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/stats-count-up-row.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/stats-count-up-row" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More stats

- [Stats Bar Race](https://pixelpapa.com/components/stats/stats-bar-race.md) (Pro): A bar-chart race: horizontal bars grow and re-rank with layout springs as the year advances on its own, with a play/pause control and a year scrubber.
- [Stats Grid Bordered](https://pixelpapa.com/components/stats/stats-grid-bordered.md): A minimal hairline grid of serif numerals that rise through a mask on view; hovering a cell floods it with the foreground colour from the bottom edge and reveals a footnote.
- [Stats Radial Progress](https://pixelpapa.com/components/stats/stats-radial-progress.md): Four concentric rings sweep to their values with spring easing beside a legend; hovering a row thickens its ring, dims the rest and swaps the centre readout.
- [Stats Split Quote](https://pixelpapa.com/components/stats/stats-split-quote.md) (Pro): A split section: an oversized serif stat counts up on the left with a before/after bar, while the customer quote on the right reveals word by word.
- [Stats Ticker Odometer](https://pixelpapa.com/components/stats/stats-ticker-odometer.md) (Pro): A live counter built from rolling digit columns: each digit springs to its new value as the total ticks up, with three smaller odometers underneath.
- [Stats With Sparklines](https://pixelpapa.com/components/stats/stats-with-sparklines.md) (Pro): Four stat cards whose sparklines draw in on view; move the pointer across a card to scrub its history and the headline number follows the marker.
