Progress Ring

Animated

A radial progress arc, spring-driven, with an indeterminate sweep.

View Source

#Installation

npx @dinachi/cli@latest add progress-ring

#Usage

tsx
import { ProgressRing } from "@/components/ui/progress-ring"
tsx
<ProgressRing value={64} label="Upload" />
<ProgressRing label="Fetching" />

#Examples

#Why a spring

Progress arrives in jumps. A tween restarted on every jump either stutters or has to be cancelled and rebuilt. A spring re-targets from wherever the arc currently is and carries its velocity across, so a fast run of updates reads as one accelerating sweep instead of a series of separate moves.

The number in the middle reads the same spring as the arc, so the two can never disagree. It is the same spring the number ticker uses, so a ring and a count beside each other travel on one clock.

#The indeterminate arc sweeps its own length

A fixed arc spinning at a constant rate is a loading spinner, and it says only "still running". Letting the arc lengthen and shorten under the rotation is what makes it read as work happening rather than as a wheel turning: the sweep and the rotation are deliberately different lengths, so the pattern never lands in the same place twice in a row.

Omit value to get it. A ring that reports a number it is guessing at is worse than one that admits it does not know.

#The arc repaints

The arc is stroke-dashoffset, which is not a compositor property. At this size that is fine: one small element repainting a stroke. Forty of them on a page would be a different conversation, and the alternative is a rotated half-disc mask that animates on transform but cannot do a rounded cap cleanly.

#When to use it

Use it forNot for
A single operation the reader is waiting on: an upload, an import, a quota.A list of rows each with their own progress. Use Progress: a bar packs into a row, and forty repainting strokes do not.
Compact places where a bar has nowhere to run.Anything under about 40px. The number stops fitting before the ring does.

#Accessibility

  • Determinate rings are a progressbar with aria-valuenow, aria-valuemin, aria-valuemax and a spoken aria-valuetext.
  • Indeterminate rings are a status with no value, because there is none.
  • label is the accessible name, and defaults to "Progress" or "Loading".
  • Reduced motion has the arc report the value rather than travel to it, and replaces the endless sweep with a static gap. An animation that repeats forever is the thing the preference most clearly means to stop.

#API Reference

PropTypeDefaultDescription
valuenumber0–100. Omit it for the indeterminate arc
labelstring"Progress" / "Loading"Accessible name
sizenumber96Outer diameter in px
thicknessnumberStroke width in px. Scales with size unless given
showValuebooleantrueShow the percentage in the middle. Ignored when indeterminate — there is no number to show