Overview
<Progress /> is the linear bar (determinate + indeterminate). <CircularProgress />
is the SVG ring variant. Both share the same prop vocabulary (value, min, max, variant,
size, color, indeterminate, showLabel, labelFormat, animated) and the same ARIA
semantics — CSS-only animations with no external motion library.
Overview — indeterminate, 35%, and 80% bars
Variants
Linear
solid— neutral track + role-colored bar. Default.soft— role-subtletrack + role-colored bar. The bar sits inside its own tinted lane.striped—solidplus a diagonal CSS-gradient overlay on the bar. Reach for it on long-running uploads / installs to communicate "active work".
Circular
solid—bg-subtletrack + role-colored arc. Default.soft— role-subtletrack + role-colored arc.
(No striped for circular — SVG strokes can't carry a diagonal pattern without a <pattern>
def.)
Sizes
| Token | Linear height | Circular diameter | Default thickness |
|---|---|---|---|
sm | h-1 (4px) | 24px | 3px |
md | h-2 (8px) | 40px | 4px |
lg | h-3 (12px) | 56px | 5px |
<CircularProgress size={number} /> sets the diameter directly; thickness defaults to
Math.max(2, size / 10) and can still be overridden via thickness.
Anatomy
Linear
[ track | bar { stripes? | label? } ][ track | bar { stripes? | label? } ]Circular
[ root
↳ svg
↳ <circle/> (track)
↳ <circle/> (arc — strokeDasharray drives the visible length)
↳ label? ][ root
↳ svg
↳ <circle/> (track)
↳ <circle/> (arc — strokeDasharray drives the visible length)
↳ label? ]Examples
Basic
BasicCircular
Variants
Sizes
Colors
Indeterminate
WithLabel
CustomFormat
Rounded
ThicknessCircular
CustomSize
Animated
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| animated | boolean | true | Animate the bar's width transition when `value` changes. Honors `prefers-reduced-motion` (the Tailwind preset wires that for the indeterminate animation). |
| color | ResponsiveValue<ProgressColor> | 'primary' | Palette role driving the bar fill. |
| indeterminate | boolean | — | When `true`, the bar paints a CSS-driven sweeping animation and `aria-valuenow` is omitted — the correct ARIA semantics for "we don't know how far along we are" progress. |
| labelFormat | ProgressLabelFormatter | — | Custom formatter. Falls back to `Math.round(percent) + '%'` when omitted. |
| max | number | 100 | Range maximum. |
| min | number | 0 | Range minimum. |
| rounded | ResponsiveValue<ProgressRounded> | 'full' | Corner-radius family. |
| showLabel | boolean | false | Render the percentage (or `labelFormat` output) inside the track. The label is `aria-hidden` because the percentage is already exposed via `aria-valuenow` / `aria-valuetext`. |
| size | ResponsiveValue<ProgressSize> | 'md' | Track height / label size. |
| striped | boolean | false | Diagonal stripes overlay. Setting this to `true` is equivalent to `variant='striped'`; both paths land on the same compound rule. |
| sx | Sx | — | Theme-aware inline style object (resolves palette / spacing / radius tokens to CSS vars). |
| value | number | 0 | Current value. Clamped into `[min, max]` before being painted. Ignored when `indeterminate` is `true`. |
| variant | ResponsiveValue<ProgressVariant> | 'solid' | Stylistic family. |
Accessibility
- Both surfaces render
role="progressbar"on the root element. aria-valuemin,aria-valuemax,aria-valuenoware emitted when determinate.aria-valuetextis always present — defaults to thelabelFormatoutput when provided, otherwise the rounded percentage. Indeterminate emitsaria-valuetext="Loading"and omitsaria-valuenow(the correct ARIA for "we don't know how far along we are").- The visible inline / centered label is
aria-hiddenbecause the same value is already exposed viaaria-valuenow/aria-valuetext. - The inner
<svg>of<CircularProgress />isaria-hidden— the wrapper carries the accessible name. - Pass
aria-label(or usearia-labelledbyto point at a heading) so SR users hear what is progressing. axe-core: zero violations across all 21 + 14 = 35 variant×color×size cells.
Animation / Reduced motion
- Determinate:
width(linear) /stroke-dashoffset(circular) transition overduration-normalwithease-emphasized. Passanimated={false}to opt out per-instance — the bar/arc snaps. Themotion-reduce:Tailwind variant also halts the transition for users withprefers-reduced-motion: reduce. - Indeterminate (linear):
progress-indeterminatekeyframe slides the 1/3-width bar across the track on a 1.4s loop. - Indeterminate (circular):
circular-indeterminate-spinrotates the whole SVG; in parallelcircular-indeterminate-dashmodulatesstroke-dashoffsetso the arc grows then shrinks. Both loops are 1.4s. - All four keyframes live in
packages/theme/src/tailwind-preset.ts. - Reduced-motion fallback: indeterminate animations halt; the static state drops to 60% opacity so the surface still reads as "in progress" without motion.
Theming
Every visual decision is a palette token. Pull the <ThemeProvider> knobs to remix the whole
library, or scope overrides to Progress / CircularProgress specifically:
<ThemeProvider
theme={defineTheme({
components: {
Progress: {
defaultProps: { variant: 'soft', rounded: 'md' },
styleOverrides: {
track: '',
bar: 'opacity-90',
label: 'tracking-wide',
},
},
CircularProgress: {
defaultProps: { thickness: 5 },
styleOverrides: {
root: '',
svg: '',
track: 'opacity-30',
arc: '',
label: 'tabular-nums',
},
},
},
})}
>
{children}
</ThemeProvider><ThemeProvider
theme={defineTheme({
components: {
Progress: {
defaultProps: { variant: 'soft', rounded: 'md' },
styleOverrides: {
track: '',
bar: 'opacity-90',
label: 'tracking-wide',
},
},
CircularProgress: {
defaultProps: { thickness: 5 },
styleOverrides: {
root: '',
svg: '',
track: 'opacity-30',
arc: '',
label: 'tabular-nums',
},
},
},
})}
>
{children}
</ThemeProvider>Instance-level overrides win via tailwind-merge:
<Progress className="rounded-none" style={{ minWidth: 240 }} value={66} />
<CircularProgress sx={{ color: 'success.main' }} value={66} /><Progress className="rounded-none" style={{ minWidth: 240 }} value={66} />
<CircularProgress sx={{ color: 'success.main' }} value={66} />Do / Don't
- Do prefer
indeterminatewhenever you can't measure the operation. A bar pegged at "5%" for two minutes is worse than a sweeping indeterminate bar. - Do pair
showLabelwithlabelFormatfor domain-specific units ("2.4 GB / 4 GB","step 3 of 5") — the percent is rarely the most useful number on its own. - Don't stack more than one indeterminate spinner on a single screen — the motion competes. A single page-level spinner reads as authoritative; many small ones read as broken.
- Don't flip
animated={false}globally in a theme override unless you've also disabled the indeterminate keyframes — the result is a static bar that lies about its state.