Progress Radial
A circular progress ring for dashboards and stat tiles, where progress is the same idea as a bar.
The ring is one conic-gradient masked into an annulus and driven by a single
custom property — no canvas, no SVG arc math. A value change is one style write.
Sizes
Four sizes, from an inline sm ring beside a label to an xl hero ring on a
dashboard. Only the size changes below — same value, same intent.
Intents
The color axis is the one used across the library — primary by default, plus the
state colors. Same value and size below, so only the color changes.
Animation
Pass animate to sweep the fill from empty to its value on first paint — and to
animate later value changes instead of snapping. Use the preview's replay control
to watch it again.
It is pure polish: the sweep is gated by prefers-reduced-motion, and a browser
that doesn't support the on-load animation simply shows the ring at its value. The
ring is complete either way.
The value and what it is out of
value and max work like the linear bar. The sweep comes from the ratio, so
a max that is not 100 still draws correctly:
{{-- Half a ring — not 30% --}}
<x-wirekit::radial-progress :value="30" :max="60" label="Tasks done" />
A value above max is clamped rather than drawn: a cone past a full turn would
start overdrawing the ring it already painted. A max of 0 is treated as 100
instead of dividing by zero.
What screen readers hear
The ring is a real role="progressbar" with aria-valuenow / aria-valuemin /
aria-valuemax, and label is required — a progressbar with no name
announces a number and never says what the number is about.
By default assistive tech hears the percentage, which is right when the percentage is the point. When it is not, say what the number means:
<x-wirekit::radial-progress
:value="18.2"
:max="25"
label="Storage"
value-text="18.2 GB of 25 GB used"
>
73%
</x-wirekit::radial-progress>
The center is a slot and starts empty — a bare percentage repeated inside
every ring is noise that aria-valuetext already carries. Put something there
when it earns its place: a real figure, a unit, an icon.
18.2 GB
of 25
Threshold coloring
warn and danger are fractions of max. Crossing one overrides the
declared intent:
{{-- Turns warning at 80%, danger at 100% --}}
<x-wirekit::radial-progress :value="85" label="Quota" intent="primary" :warn="0.8" :danger="1.0" />
The same ring, same intent="primary", at three values — the color follows the
threshold, not the declared intent:
A ring that is over its limit but still drawn in your brand color is telling the reader the wrong thing, whatever the author declared — so the threshold wins.
Thresholds are opt-in: without them, 99% of a download is not a warning.
The thresholds mirror usage meter, so a dashboard reads the same in both shapes.
Color is never the only signal Threshold coloring must not be the only thing that says a limit was crossed. A reader who cannot separate the warning hue from the brand one sees an identical ring. Put the state in text too — in the center, in the label, or in a badge beside it.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
value |
int|float |
0 |
Current value. Clamped to [0, max]. |
max |
int|float |
100 |
What the value is out of. A non-positive max is treated as 100. |
label |
string|null |
null |
Accessible name. Required for the ring to mean anything. |
valueText |
string|null |
null |
What assistive tech reads instead of the percentage. |
size |
string |
'md' |
sm, md, lg, xl. |
intent |
string |
'primary' |
primary, accent, success, warning, danger, info, neutral. |
warn |
float|null |
null |
Fraction of max that turns the ring warning. |
danger |
float|null |
null |
Fraction of max that turns the ring danger. |
animate |
bool |
false |
Sweep the fill from empty to its value on first paint, and animate later value changes. Gated by prefers-reduced-motion. |
scope |
string|null |
null |
Class-scope override. |
Accessibility
- A real
role="progressbar"witharia-valuenow/aria-valuemin/aria-valuemaxand anaria-valuetextthat says what the value means. aria-valuetextand the visible center are the same string when you slot the same text, so they cannot disagree.- The sweep transition is dropped under
prefers-reduced-motion. infoshares the accent fill: there is no distinct info base token, and the linear bar makes the same call.
Keyboard Interaction
None. A progress ring reports; it is not a control. Nothing here is focusable.
Design Tokens
The ring reads two variables you can set per instance or per theme:
| Variable | Purpose |
|---|---|
--wk-radial-size |
Outer diameter. The size prop sets it. |
--wk-radial-thickness |
Ring thickness. Scales with size; override for a hairline or a heavy ring. |
/* A thinner ring everywhere */
.wk-radial {
--wk-radial-thickness: 0.25rem;
}
The track uses --color-wk-bg-muted — the same track color the linear bar uses,
so the two shapes read as one family.