# Stats Radial Progress — React Stats Section

> 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.

- Category: [Stats](https://pixelpapa.com/components/stats.md)
- Tags: stats, radial, rings, progress, legend
- Dependencies: motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/stats/stats-radial-progress

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/stats-radial-progress.json"
```

The file is written to `components/pixelpapa/stats-radial-progress.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 { motion, useInView } from "motion/react";
import { useRef, useState } from "react";
import { cn } from "@/lib/utils";

type Ring = { label: string; value: number; detail: string; color: string };

const defaultRings: Ring[] = [
  { label: "Renewable energy", value: 92, detail: "Of all electricity across our four data centres", color: "var(--chart-5)" },
  { label: "Hardware reused", value: 78, detail: "Servers refurbished and redeployed after five years", color: "var(--chart-2)" },
  { label: "Water recycled", value: 64, detail: "Closed-loop cooling, measured per megawatt hour", color: "var(--chart-1)" },
  { label: "Suppliers audited", value: 47, detail: "On track for every tier-one supplier by next June", color: "var(--chart-4)" },
];

const SIZE = 320;
const STROKE = 18;
const GAP = 10;

export default function StatsRadialProgress({ rings = defaultRings }: { rings?: Ring[] }) {
  const ref = useRef<HTMLDivElement>(null);
  const inView = useInView(ref, { once: true, amount: 0.4 });
  const [hover, setHover] = useState<number | null>(null);
  const focus = hover === null ? null : rings[hover];
  const average = Math.round(rings.reduce((sum, r) => sum + r.value, 0) / rings.length);

  return (
    <section className="w-full bg-background px-6 py-20 sm:py-24">
      <div ref={ref} className="mx-auto grid max-w-5xl items-center gap-12 lg:grid-cols-[auto_1fr] lg:gap-20">
        <div className="relative mx-auto size-72 shrink-0 sm:size-80">
          <svg viewBox={`0 0 ${SIZE} ${SIZE}`} className="-rotate-90" fill="none">
            {rings.map((ring, i) => {
              const r = SIZE / 2 - STROKE / 2 - i * (STROKE + GAP);
              const c = 2 * Math.PI * r;
              const dim = hover !== null && hover !== i;
              return (
                <g key={ring.label} className="transition-opacity duration-300" opacity={dim ? 0.25 : 1}>
                  <circle cx={SIZE / 2} cy={SIZE / 2} r={r} stroke="var(--muted)" strokeWidth={STROKE} />
                  <motion.circle
                    cx={SIZE / 2}
                    cy={SIZE / 2}
                    r={r}
                    stroke={ring.color}
                    strokeLinecap="round"
                    strokeDasharray={c}
                    initial={{ strokeDashoffset: c, strokeWidth: STROKE }}
                    animate={{ strokeDashoffset: inView ? c * (1 - ring.value / 100) : c, strokeWidth: hover === i ? STROKE + 6 : STROKE }}
                    transition={{
                      strokeDashoffset: { type: "spring", stiffness: 40, damping: 14, delay: i * 0.12 },
                      strokeWidth: { type: "spring", stiffness: 400, damping: 24 },
                    }}
                  />
                </g>
              );
            })}
          </svg>
          <div className="absolute inset-0 flex flex-col items-center justify-center text-center">
            <span className="font-serif text-5xl text-foreground tabular-nums">{focus ? focus.value : average}%</span>
            <span className="mt-1 max-w-24 text-xs text-muted-foreground">{focus ? focus.label : "average progress"}</span>
          </div>
        </div>

        <div>
          <p className="text-sm font-medium text-primary">2030 commitments · Basalt Compute</p>
          <h2 className="mt-3 text-4xl font-semibold tracking-tight text-balance text-foreground sm:text-5xl">How far along we actually are</h2>
          <ul className="mt-8 divide-y divide-border border-y border-border">
            {rings.map((ring, i) => (
              <li
                key={ring.label}
                tabIndex={0}
                onPointerEnter={() => setHover(i)}
                onPointerLeave={() => setHover(null)}
                onFocus={() => setHover(i)}
                onBlur={() => setHover(null)}
                className={cn(
                  "flex cursor-default items-center gap-4 py-4 outline-none transition-[opacity,padding] duration-300 focus-visible:bg-muted/50",
                  hover !== null && hover !== i && "opacity-50",
                  hover === i && "pl-2",
                )}
              >
                <span className="size-3 shrink-0 rounded-full" style={{ background: ring.color }} />
                <span className="min-w-0 flex-1">
                  <span className="block font-medium text-foreground">{ring.label}</span>
                  <span className="block text-sm text-muted-foreground">{ring.detail}</span>
                </span>
                <span className="font-serif text-3xl text-foreground tabular-nums">{ring.value}%</span>
              </li>
            ))}
          </ul>
          <p className="mt-4 text-xs text-muted-foreground">Verified by an independent auditor each January.</p>
        </div>
      </div>
    </section>
  );
}
```

## AI prompt

```text
Add the "Stats Radial Progress" component from PixelPapa UI to this project.

What it is: 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.
Category: Stats. Depends on: motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/stats-radial-progress.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/stats-radial-progress.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/stats-radial-progress" 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 Count Up Row](https://pixelpapa.com/components/stats/stats-count-up-row.md): 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.
- [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 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.
