Skip to content

CircularProgress

The same task status as Progress, drawn as a ring. It takes a fixed square instead of a full-width row, which is what makes it the shape for a button, a card corner, an avatar overlay — anywhere there is no line to span.

Uploadingx
import { CircularProgress } from '@ontoui/react';
<CircularProgress.Root value={40} size={72}>
<CircularProgress.Track>
<CircularProgress.Indicator />
</CircularProgress.Track>
<CircularProgress.Value />
<CircularProgress.Label>Uploading</CircularProgress.Label>
</CircularProgress.Root>;

Anatomy

PartDescription
CircularProgress.Root<div role="progressbar"> — holds the value and the geometry, and announces it.
CircularProgress.Track<svg> — the rail, and the canvas the indicator is drawn into.
CircularProgress.Indicator<circle> — the arc. Must sit inside the track.
CircularProgress.Value<span> — the value as text, centred in the ring.
CircularProgress.Label<span> — names the task. Becomes the ring’s accessible name.

The value is centred over the ring and the label sits beneath it, whatever order the parts are written in — the two share a grid cell with the ring rather than being positioned against it.

Both the ring and the value are optional. A spinner is a root with a track and an indicator and nothing else.

Value

value is required, and it is always controlled. Intl.NumberFormat options go through format, and min and max set the scale — all of it exactly as on Progress, including the default reading of the value as a percentage of 100.

<CircularProgress.Root value={3} max={5} format={{ style: 'decimal' }}>
<CircularProgress.Track>
<CircularProgress.Indicator />
</CircularProgress.Track>
<CircularProgress.Value>{(formatted) => `${formatted}/5`}</CircularProgress.Value>
</CircularProgress.Root>

The arc starts at twelve o’clock and grows clockwise, and it is stroked from the value against min and max — so the ring is at a quarter turn when the value is a quarter of the way along, whatever the text in the middle says.

Keep the centred text short. It is bounded by the ring’s inner diameter, and 40% or 3/5 fits where 840 MB of 1,024 MB does not — that belongs in a label beside the ring.

Indeterminate

value={null} says the task is running and its progress is unknown: the ring drops aria-valuenow, announces itself as indeterminate, and spins.

x
x
Loadingx
Without a value the ring is a spinner: it reports work, not progress.
<CircularProgress.Root value={null} size={20} thickness={3}>
<CircularProgress.Track>
<CircularProgress.Indicator />
</CircularProgress.Track>
</CircularProgress.Root>

This is the library’s spinner. A determinate ring is better whenever the work can be measured — a spinner says nothing about whether it is nearly done or has barely started.

Size

x
x
x
<CircularProgress.Root value={65} size={96} thickness={10}>
<CircularProgress.Track>
<CircularProgress.Indicator />
</CircularProgress.Track>
<CircularProgress.Value />
</CircularProgress.Root>

size is the diameter in pixels and thickness is the stroke, both props rather than CSS because the SVG geometry is computed from them: the circle is inset by half a stroke so the ring is not clipped by its own box, and the arc length is derived from the radius that leaves.

size defaults to 40 and thickness to 4. The centred value scales with the ring, so it stays in proportion from an inline 24px spinner up to a 96px card graphic — but a ring much below 40px has no room for text inside it, so give small ones a label instead.

Color

Syncingx
Syncedx
Quotax
Fullx
<CircularProgress.Root value={88} color="warning">

Color is decorative — a screen reader hears the same value at any tint. Keep the meaning in the label rather than in the color alone.

Accessibility

  • CircularProgress.Label names the ring. Without it, give CircularProgress.Root an aria-label — this matters most for the spinner, which otherwise announces only that something unnamed is in progress.
  • The ring itself is aria-hidden. It is a drawing of the value the root already announces.
  • CircularProgress.Value is hidden from screen readers for the same reason.
  • Motion is reduced, not removed. Under prefers-reduced-motion: reduce the spinner stops turning and a quarter arc pulses in place — a still, empty ring would read as stalled.

API Reference

CircularProgress.Root

Prop Type Default
children? ReactNode
className? string
style? CSSProperties
value number | null
min? number 0
max? number 100
size? number 40
thickness? number 4
format? NumberFormatOptions
locale? LocalesArgument the runtime locale
getAriaValueText? (formattedValue: string | null, value: number | null) => string
color? "default" | "success" | "warning" | "danger" 'default'
ref? Ref<HTMLDivElement>

CircularProgress.Track

Prop Type Default
children? ReactNode
className? string
style? CSSProperties
ref? Ref<HTMLDivElement>

CircularProgress.Indicator

Prop Type Default
className? string
style? CSSProperties
ref? Ref<HTMLDivElement>

CircularProgress.Value

Prop Type Default
className? string
style? CSSProperties
children? (formattedValue: string | null, value: number | null) => ReactNode
ref? Ref<HTMLDivElement>

CircularProgress.Label

Prop Type Default
children? ReactNode
className? string
style? CSSProperties
id? string
ref? Ref<HTMLDivElement>