Home/ Cookbook/ Charts/ Progress ring

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.

Charts Intermediate ~8 min React · @corelithzw/react

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.

Monthly revenue progress $32,400 of $40,000 target — 81 percent. 81% of goal
Revenue$32,400 of $40,000
Collection rate progress $48,210 of $52,400 invoiced — 92 percent, target met. 92% on target
Collection$48,210 of $52,400
Cash on hand vs. payroll $14,200 of $24,000 payroll need — 59 percent, behind. 59% at risk
Cash for payroll$14,200 of $24,000
Revenue at 81%, on pace. Collection rate 92%, target met. Cash for payroll only 59% — at risk.

Required pieces

From @corelithzw/react.

Intended exports used here: Chart.ProgressRing.

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

92% collected
CollectionRing.tsx@corelithzw/react

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.

Collection rate · May
target 90%
92% on target
$48,210
of $52,400 invoiced
+4 pts vs. last month

Accessibility

A progress ring is a progressbar in disguise — give the assistive layer what it expects.

  • Real progressbar role. Root <svg> carries role="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 via aria-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: reduce disables the fill animation on mount.