Show one bounded value with a progress ring
By the end of this recipe you'll have a circular progress indicator with a centre label — the right chart whenever you have one number that lives between 0 and a known target.
Overview
One value vs. one target. The ring fills clockwise from 12 o'clock; the centre carries the headline number.
Progress rings are the KPI card's favourite companion. The ring gives an instant "how far through" read; the centre carries the precise value. Use them for completion against a target — bookings vs. capacity, payments vs. invoiced, runners against a step count, cash on hand vs. payroll-week need.
Skip the ring when there's no real target (a raw count belongs in a stat card, no chart needed), or when you have more than one value to show — at that point a donut handles composition better, or a small set of rings side-by-side handle multiple goals.
Default opinion: the ring tells you "yes/almost/no" at a glance. The centre tells you the exact number. Both jobs, one component, no confusion.
The chart
Three goals on one row. The colour reflects the status — brand for on-pace, success when met, danger when behind.
Required pieces
From @corelithzw/react.
Intended exports used here: Chart.ProgressRing.
Chart.ProgressRing ships in @corelithzw/react v0.2. Today, Progress already renders a linear bar with the same prop shape.
The React snippet
One value, one max, one optional status function. The component picks its own colour.
Customising
Three forks.
Half-ring gauge
Render only the top semicircle for an exec-friendly gauge look. Same data; less ink.
<Chart.ProgressRing
value={value}
max={max}
variant="gauge" {/* 0–180° arc */}
size={200}
/>
Stacked rings
Layer 2–3 metrics in concentric rings — Apple-watch style. Each ring keeps its own tone.
<Chart.ProgressRing
rings={[
{ value: 32, max: 40, tone: 'brand' },
{ value: 48, max: 52, tone: 'success' },
{ value: 14, max: 24, tone: 'danger' },
]}
size={180}
/>
Indeterminate
When the value is unknown but the action is in flight, switch to a spinning indeterminate ring.
<Chart.ProgressRing
indeterminate
size={48}
thickness={4}
aria-label="Syncing"
/>
In context
A KPI hero card. The ring sits to the left; the supporting numbers explain it.
Accessibility
A progress ring is a progressbar in disguise — give the assistive layer what it expects.
- Real progressbar role. Root
<svg>carriesrole="progressbar"+aria-valuenow+aria-valuemin+aria-valuemax+aria-valuetext("48,210 of 52,400 invoiced"). - Centre is decorative. The big number in the middle is
aria-hidden="true"— the same value is already on the root viaaria-valuetext, so it isn't read twice. - Tone is more than colour. The status word ("on target", "at risk") is rendered as text below the percentage, so colour-blind readers see the message.
- Keyboard. Rings are not focusable by default — they don't have an action. Wrap one in a link or button if the ring is clickable.
- Reduced motion.
prefers-reduced-motion: reducedisables the fill animation on mount.