# Radial Gauge — React Chart Component

> A 240° SVG gauge whose arc, end cap and centre number all ride one spring, changing colour as the value crosses its zones.

- Category: [Charts](https://pixelpapa.com/components/charts.md)
- Tags: chart, gauge, radial, meter, svg
- Dependencies: motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/charts/chart-radial-gauge

## Install

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

The file is written to `components/pixelpapa/chart-radial-gauge.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, useReducedMotion, useSpring, useTransform } from "motion/react";
import { useEffect, useState } from "react";
import { cn } from "@/lib/utils";

export type GaugeZone = { upTo: number; color: string };

type RadialGaugeProps = {
  value: number;
  min?: number;
  max?: number;
  label: string;
  unit?: string;
  /** Ascending thresholds; the arc takes the colour of the first zone the value fits in. */
  zones?: GaugeZone[];
  className?: string;
};

const R = 80;
const SWEEP = 240;
const START = 150; // degrees, clockwise from 3 o'clock
const rad = (deg: number) => (deg * Math.PI) / 180;
const polar = (deg: number, r = R) => ({ x: 100 + r * Math.cos(rad(deg)), y: 100 + r * Math.sin(rad(deg)) });

const a0 = polar(START);
const a1 = polar(START + SWEEP);
const ARC = `M${a0.x},${a0.y} A${R},${R} 0 1 1 ${a1.x},${a1.y}`;

export function RadialGauge({ value, min = 0, max = 100, label, unit = "%", zones, className }: RadialGaugeProps) {
  const reduce = useReducedMotion();
  const frac = Math.max(0, Math.min(1, (value - min) / (max - min)));
  const spring = useSpring(0, { stiffness: 60, damping: 16 });
  const cx = useTransform(spring, (f) => polar(START + f * SWEEP).x);
  const cy = useTransform(spring, (f) => polar(START + f * SWEEP).y);
  const text = useTransform(spring, (f) => String(Math.round(min + f * (max - min))));
  const color = zones?.find((z) => value <= z.upTo)?.color ?? zones?.[zones.length - 1]?.color ?? "var(--primary)";

  useEffect(() => {
    if (reduce) spring.jump(frac);
    else spring.set(frac);
  }, [frac, reduce, spring]);

  return (
    <div
      role="meter"
      aria-label={label}
      aria-valuemin={min}
      aria-valuemax={max}
      aria-valuenow={value}
      aria-valuetext={`${value}${unit}`}
      className={cn("@container relative aspect-[200/172] w-full", className)}
    >
      <svg viewBox="0 0 200 172" className="size-full overflow-visible" aria-hidden>
        {Array.from({ length: 25 }, (_, i) => {
          const deg = START + (i / 24) * SWEEP;
          const major = i % 6 === 0;
          const p = polar(deg, R - 15);
          const q = polar(deg, R - (major ? 23 : 19));
          return (
            <line
              key={i}
              x1={p.x}
              y1={p.y}
              x2={q.x}
              y2={q.y}
              strokeLinecap="round"
              strokeWidth={major ? 1.6 : 1}
              className={i / 24 <= frac ? "stroke-foreground/60" : "stroke-foreground/15"}
            />
          );
        })}
        <path d={ARC} fill="none" strokeWidth={12} strokeLinecap="round" className="stroke-muted" />
        <motion.path
          d={ARC}
          fill="none"
          strokeWidth={12}
          strokeLinecap="round"
          stroke={color}
          style={{ pathLength: spring, transition: "stroke 0.4s" }}
        />
        <motion.circle cx={cx} cy={cy} r={9} fill={color} fillOpacity={0.22} style={{ transition: "fill 0.4s" }} />
        <motion.circle cx={cx} cy={cy} r={3} className="fill-card" />
      </svg>
      <div className="pointer-events-none absolute inset-x-0 top-[38%] flex flex-col items-center">
        <p className="flex items-baseline text-[clamp(1.25rem,18cqw,2.75rem)] leading-none font-semibold tracking-tight text-foreground tabular-nums">
          <motion.span>{text}</motion.span>
          <span className="ml-0.5 text-[0.4em] font-medium text-muted-foreground">{unit}</span>
        </p>
        <p className="mt-1.5 text-xs text-muted-foreground">{label}</p>
      </div>
    </div>
  );
}

const zones: GaugeZone[] = [
  { upTo: 60, color: "var(--chart-5)" },
  { upTo: 85, color: "var(--chart-4)" },
  { upTo: 100, color: "var(--destructive)" },
];
const latencyZones: GaugeZone[] = [
  { upTo: 60, color: "var(--chart-1)" },
  { upTo: 120, color: "var(--chart-4)" },
];
// A deterministic "live" feed so the thumbnail keeps moving.
const cpuFeed = [47, 71, 58, 83, 91, 76, 49, 67];

export default function ChartRadialGaugeDemo() {
  const [tick, setTick] = useState(0);

  useEffect(() => {
    const id = setInterval(() => setTick((t) => t + 1), 2200);
    return () => clearInterval(id);
  }, []);

  const cpu = cpuFeed[tick % cpuFeed.length];

  return (
    <div className="w-full max-w-xl rounded-2xl border border-border bg-card p-6 shadow-sm">
      <div className="flex items-center justify-between">
        <div>
          <p className="font-medium text-card-foreground">edge-fra-04</p>
          <p className="text-sm text-muted-foreground">Frankfurt · 16 vCPU · 64 GB</p>
        </div>
        <span className="inline-flex items-center gap-1.5 rounded-full border border-border px-2.5 py-1 text-xs text-muted-foreground">
          <span className="size-1.5 animate-pulse rounded-full bg-emerald-500 motion-reduce:animate-none" />
          Live
        </span>
      </div>
      <div className="mt-4 grid grid-cols-[1.5fr_1fr_1fr] items-end gap-4">
        <RadialGauge value={cpu} label="CPU load" zones={zones} />
        <RadialGauge value={72} label="Memory" zones={zones} />
        <RadialGauge value={38} max={120} unit="ms" label="p95 latency" zones={latencyZones} />
      </div>
    </div>
  );
}
```

## AI prompt

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

What it is: A 240° SVG gauge whose arc, end cap and centre number all ride one spring, changing colour as the value crosses its zones.
Category: Charts. Depends on: motion.

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

- [Area Chart Gradient](https://pixelpapa.com/components/charts/chart-area-gradient.md) (Pro): A smooth SVG area chart that draws its line on mount, fades in a gradient fill and snaps a crosshair tooltip to the nearest point.
- [Donut Chart Segments](https://pixelpapa.com/components/charts/chart-donut-segments.md): A donut whose segments sweep in one after another; hovering a segment or its legend row thickens it and swaps the centre total for that slice.
- [Funnel Steps Chart](https://pixelpapa.com/components/charts/chart-funnel-steps.md) (Pro): A flowing horizontal funnel: each stage tapers into the next with a bezier, stages fade in left to right and hovering one shows its drop-off.
- [Grouped Bar Chart](https://pixelpapa.com/components/charts/chart-bar-grouped.md) (Pro): Side-by-side SVG bars per category that grow from the baseline in a stagger, with a group highlight and a tooltip comparing every series.
- [Heat Grid Week by Hour](https://pixelpapa.com/components/charts/chart-heat-grid.md) (Pro): A 7×24 SVG heatmap whose cells ripple in diagonally; hover or arrow-key between cells for a snapped tooltip, with peak hour called out.
- [Inline Sparkline](https://pixelpapa.com/components/charts/chart-sparkline-inline.md): A tiny table-cell sparkline that draws itself on mount, colours by trend and snaps a value bubble to the nearest point on hover.
