Data Storytelling

Gauge Meter

Needle gauge: a spring-swinging needle, banded color arcs, and a rolling value.

An SVG semicircular gauge whose needle swings to the target value on a low-damping spring, overshooting and settling back. Banded color arcs (green/amber/red, with customizable stops) sit alongside tick marks, the central value shares the same spring as the needle so the two roll in sync, and the color bands the needle has not yet reached are dimmed.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add gauge-meter.en

New project or installation trouble? Start here →

Gauge Meter is a React needle gauge (a gauge chart or speedometer-style dial) built with SVG and Motion's useSpring. A segmented half-circle arc with ticks, a needle that springs to the target with a slight overshoot, a center number that counts along with it, and a status label for the segment the value is in.

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

72
240
0
Active edited usageDefault props
<GaugeMeter/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

When to use

  • CPU, memory and disk usage on a system monitoring dashboard
  • Site performance or SEO health scores
  • Temperature, humidity or pressure readings from IoT devices
  • Targets like sales attainment or customer satisfaction with "good / caution / alert" zones
  • Not a good fit: for a plain percentage without ticks or a needle, Circular Progress is simpler

Installation

npx webberui@latest add https://webberui.com/r/gauge-meter.en.json

Or, once registries are configured in components.json, install it as @webberui/gauge-meter.

Usage

import { GaugeMeter } from "@/components/ui/gauge-meter";

<GaugeMeter
  value={72}
  unit="%"
  label="CPU usage"
  stops={[
    { to: 60, color: "#10b981", label: "Smooth" },
    { to: 85, color: "#f59e0b", label: "Elevated" },
    { to: 100, color: "#ef4444", label: "Overloaded" },
  ]}
/>

Examples

Live updates from a polled API

Every time value changes, the needle springs from its current position to the new value, which suits monitoring data polled on an interval.

"use client";

import * as React from "react";
import { GaugeMeter } from "@/components/ui/gauge-meter";

export function MemoryGauge() {
  const [usage, setUsage] = React.useState(0);

  React.useEffect(() => {
    const load = () =>
      fetch("/api/metrics/memory")
        .then((r) => r.json())
        .then((d: { percent: number }) => setUsage(d.percent));
    load();
    const id = setInterval(load, 5000);
    return () => clearInterval(id);
  }, []);

  return <GaugeMeter value={usage} unit="%" label="Memory usage" decimals={1} />;
}

Custom range and segments

min and max don't have to be 0 and 100. stops are sorted by to, and each segment's label shows when the needle is inside it. The exported GaugeStop type helps declare the config.

import { GaugeMeter, type GaugeStop } from "@/components/ui/gauge-meter";

const tempStops: GaugeStop[] = [
  { to: 18, color: "#3b82f6", label: "Cold" },
  { to: 26, color: "#10b981", label: "Normal" },
  { to: 40, color: "#ef4444", label: "Overheating" },
];

<GaugeMeter value={23.5} min={0} max={40} stops={tempStops} unit="°C" label="Server room" decimals={1} />

Several gauges in a row

size sets the width and the height scales at 200:148, so gauges line up neatly in a grid.

<div className="grid grid-cols-3 gap-6">
  <GaugeMeter size={180} value={42} unit="%" label="CPU" />
  <GaugeMeter size={180} value={68} unit="%" label="Memory" />
  <GaugeMeter size={180} value={91} unit="%" label="Disk" />
</div>

Props

PropTypeDefaultDescription
valuenumber—The target value; when it changes, the needle swings there on a spring (overshooting and settling back)
minnumber0Minimum of the gauge
maxnumber100Maximum of the gauge
stopsGaugeStop[]green/amber/red defaultBanded color arc configuration, each band { to, color, label? }; when omitted, green/amber/red at 60% / 85% / 100% is used
unitstring—Unit text after the value, such as "%" or "km/h"
labelstring—Gauge title, shown below the gauge
sizenumber240Gauge width (px); the height scales automatically at a 200:148 ratio
decimalsnumber0Decimal places shown for the central value
classNamestring—Forwarded to the outer container

GaugeStop: to is the end value of that color band (the bands must increase, and the last one should ideally equal max), color is any valid CSS color, and label is the status text shown when the current value falls inside that band.

Accessibility

  • The container uses role="meter" together with aria-valuemin / aria-valuemax / aria-valuenow and aria-valuetext, so assistive technology announces the target value rather than an intermediate animation value
  • The SVG itself is marked aria-hidden to avoid announcing the same thing twice alongside the meter semantics
  • When the user has "reduce motion" enabled at the system level, the needle and the number jump straight to the target value with no spring swing played
  • The value uses tabular-nums monospaced figures, so the layout does not judder while it rolls

FAQ

Does it work with the Next.js App Router?

Yes. The source already has "use client", so import it from a Server Component page. On the server and first render the needle rests at min, and it springs to the target after mount.

What happens with values outside min / max?

value is clamped to the range. During spring overshoot the needle may swing slightly past either end (about 4%), while the center number always stays within range.

How are segments split when stops is omitted?

Three segments: green, amber and red, with boundaries at 60% and 85% of the range and the status labels "Good", "Attention" and "Warning".

Can the width follow its container?

size is applied in px as an inline width on the wrapper, and the SVG itself is w-full. For a fluid width, change style={{ width: size }} in the source to the sizing you want.

What do screen readers announce?

The wrapper is role="meter" with aria-valuemin, aria-valuemax, aria-valuenow and aria-valuetext, so it reads the target value rather than the animating number. Dependencies are motion, clsx and tailwind-merge.