ProgressCircle
How far along something is, drawn as a ring.
Overview
How far along something is, drawn as a ring.
First example
<ProgressCircle value={72}><ProgressCircle.Indicator /><ProgressCircle.Value /></ProgressCircle><ProgressCircle value={100} variant="success"><ProgressCircle.Indicator /><ProgressCircle.Value>✓</ProgressCircle.Value></ProgressCircle>
Anatomy
<ProgressCircle value={72}>
<ProgressCircle.Indicator />
<ProgressCircle.Value />
</ProgressCircle>
ProgressCircle— the root. It clamps the value, resolves the recipe and computes the ring's geometry once, then publishes all three.ProgressCircle.Indicator— the ring: the track, and the arc of it that is done.ProgressCircle.Value— how far along, written in the middle.
Written with no children it is the ring alone, which is the form a row wants.
It is the ProgressBar bent into a circle — the same
five variants, the same clamped range, the same formatOptions, the same 240ms — and it is
a second component rather than a shape prop on that one because the two share no geometry
at all: a bar is a View that grows, and this is an SVG path whose dash offset moves.
There is no isIndeterminate here either. The reason is the bar's, and it is written up
there.
Usage
The number in the middle
<ProgressCircle value={72}>
<ProgressCircle.Indicator />
<ProgressCircle.Value />
</ProgressCircle>
<ProgressCircle value={100} variant="success">
<ProgressCircle.Indicator />
<ProgressCircle.Value>✓</ProgressCircle.Value>
</ProgressCircle>
Value is positioned absolutely so it centres on the ring rather than pushing it: the root
is a box the size of the circle, and a text node in the flow would make that box taller than
the drawing in it.
It formats through the root's formatOptions, on the same rule as the bar's — the fraction
for a percentage, the value for anything else. Children replace it outright.
Not at sm. 32 points across, less two strokes, is narrower than "64 %" is wide. That
step is the ring on its own.
The motion
The arc sweeps to each new length over 240ms — the bar's number, because two progress indicators on one screen moving at two speeds is a bug nobody files and everybody sees.
It is a dash offset on one path rather than a shape rebuilt per value, which is what
keeps one rounded cap at each end while it moves, and it is an animated prop rather than
an animated style, because strokeDashoffset is an SVG attribute.
<ProgressCircle.Indicator animation={false} />
The ring starts at twelve o'clock. The turn is on the indicator's wrapper rather than on
each path: Circle's own originX / originY / rotation emit an invalid DOM property on
web, and one rotation on the box is one thing to read instead of three props on two circles.
Root props
ProgressCircleProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| variant | ProgressCircleVariant | undefined | No | — |
| size | ProgressCircleSize | undefined | No | The ring's diameter, its stroke and the type inside it. |
| radius | number | undefined | No | The ring's radius **in points**, and the one place in this library where `radius` means what it means in geometry rather than a corner — a circle has no corner to round. A raw number, so it lives outside the style cache and outside the vocabulary (R6): it wins over `size` the way a raw `color` wins over a variant's token. Reach for it when the ring has to line up with something already on the screen; reach for `size` otherwise. |
| strokeWidth | number | undefined | No | The stroke's thickness in points, overriding `size`. Clamped to half the diameter. |
| color | string | undefined | No | A raw tint (`'#7c3aed'`), never a token (R7). It lands on the arc. |
| value | number | undefined | No | How far along. Clamped into `[minValue, maxValue]`. |
| minValue | number | undefined | No | — |
| maxValue | number | undefined | No | — |
| formatOptions | NumberFormatOptions | undefined | No | How `ProgressCircle.Value` reads the fraction. |
| isDisabled | boolean | undefined | No | Dims the ring. There is nothing to press, so nothing else changes. |
| style | StyleProp<ViewStyle> | No | — |
| children | ReactNode | No | — |
Slots
ProgressCircleIndicatorProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| animation | boolean | undefined | No | `false` snaps the arc to its new length instead of sweeping to it. |
| style | StyleProp<ViewStyle> | No | — |
ProgressCircleValueProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
Variants, sizes and colour
Sizes
size | Diameter | Stroke | Value type |
|---|---|---|---|
sm | 32 | 3 | 12/16 |
md | 48 | 4 | 14/20 |
lg | 64 | 5 | 16/24 |
Bigger than the Spinner's ladder at every step, and that is the
difference between the two: a spinner says "wait" and this says how long, which usually
means it has a number written in the middle of it.
The stroke is centred on the path, so the ring fits its box: the path's radius is half the diameter less half the stroke.
Variants and colour
The ProgressBar's table, unchanged — one neutral ring, and five arcs on it. color is a
raw value (R7) and lands on the arc through the same bgSelected role.
The two colours reach the SVG as strings, not styles: a path is stroked by a prop rather
than by a stylesheet. The recipe still owns them, on two slots it never renders — the same
thing the Tabs recipe does with its content slot — because that is what makes a raw
color reach the arc through resolveTint, which only maps roles a variant declared.
Accessibility
accessibilityRole="progressbar"on the root, overridable.accessibilityValuecarries the caller's range and value, not the clamped fraction.- The ring itself is a drawing; what a screen reader reads is that value.
Migration from legacy
Implementation notes
`radius` is a number here
<ProgressCircle radius={28} value={72} />
The one place in this library where radius means what it means in geometry, because a
circle has no corner to round. It is a raw number, so it lives outside the style cache and
outside the vocabulary (R6): it wins over size exactly as a raw color wins over a
variant's token.
Reach for it when the ring has to line up with something already on the screen — an avatar,
a row's height. Reach for size otherwise: the ladder is the vocabulary.
strokeWidth is the same kind of prop, and both are clamped. A stroke thicker than the ring
is wide would draw a path with a negative radius, which renders nothing, on one platform,
with no error.
`react-native-svg`
An arc with a rounded cap is a stroked path, so ProgressCircle.Indicator imports
react-native-svg — an optional peer. The alternative in plain views is two rotated
half-discs clipped by a third, which cannot round its own ends.
Only that one file imports it, and this component is its own subpath export
(@xaui/native/progress-circle), so a project that never renders a ring never pays for it.
The Select's check and the Icon's chevron are already in the same position.
Alignment with the reference implementation
Identical: the three sizes, the clamped range, formatOptions, the SVG ring with a
rounded cap starting at twelve o'clock, and Indicator · Value as slots.
Three deltas:
| Theirs | Ours | Why |
|---|---|---|
size={40} — the ladder takes a number | size is tokens, radius is the number | R6: a vocabulary prop takes tokens or it is not a vocabulary. The raw escape hatch gets its own name, like color does. |
strokeWidth / trackColor / fillColor on the Indicator | strokeWidth on the root, colours from the recipe | R1 — colours are the recipe's, and variant plus color is the way in. |
color="success" | variant="success" | Their color is our variant, as on the bar. |
Extending it
useProgressCircle() is exported (R10) and carries the resolved geometry, the two stroke
colours, the fraction and the value — enough to draw a second arc, a mark at the target, or
an icon in place of the number. Outside a <ProgressCircle> it throws by name.