Progress
A bar for how far through a task or a set of steps the reader has got, built on Base UI Progress and never used to draw a health value.
Preview
progress · base base · style base-lyraOpen under the product themeInstallation
pnpm dlx shadcn@latest add @opsinjs/progress
The @opsinjs namespace is declared in your components.json. Everything it installs is code you then own. There is no runtime package to keep in step.
Usage
import { Progress } from "@/components/ui/progress"<Progress label="Uploading photos" value={45} />When to use it
Use it when
- Showing how far through an upload or a download the reader has got, where the amount done is a real fraction of a known whole.
- Tracking a multi-step flow, such as step three of five, by passing the step count as value and the total as max.
- Signalling that a task with no known shape is running, by passing value={null}, rather than inventing a percentage.
Do not use it when
- You are drawing a health reading against a range. A bar implies a target the reading does not have and carries no reference bounds.Use
range-barinstead. - You are showing a composite score. A bar reads it as a mark out of its top, the one reading a score must not invite.Use
score-dialinstead. - The wait has no bar-shaped answer, because you cannot say what is arriving as a fraction. A placeholder of the shape to come says more.Use
skeletoninstead.
Anatomy
ProgressThe root, a Base UI Progress.Root with role="progressbar" and the aria value attributes. Neutral chrome only, not focusable.Controlled byvalueProgress.LabelThe visible task name, wired by Base UI as the progressbar's accessible name. Comes from the required label prop.Controlled bylabelProgress.ValueThe text readout, formatted as a percentage. For an indeterminate task a plain phrase replaces it.Progress.TrackThe groove the fill runs in: the neutral muted surface, clipping its fill to a pill.Progress.IndicatorThe fill, in the primary action role. Its width follows the value, or spans the track and pulses when indeterminate.
- Progress 1data-slot="progress", role="progressbar"
- Progress.Label 1data-slot="progress-label", the accessible name
- Progress.Value 1data-slot="progress-value", percentage or phrase
- Progress.Track 1data-slot="progress-track", the muted groove
- Progress.Indicator 1data-slot="progress-indicator", the primary fill
Examples
A task completing
A fictional upload part way done. value sets the fill, and the readout writes
the same figure beside it for a reader who cannot use the fill.
progress-a-task-completing · base base · style base-lyraOpen under the product themeAn indeterminate task
value={null} for a task with no known shape. The fill spans the track and
pulses, the readout says the work is in progress, and Base UI drops
aria-valuenow so a screen reader announces a busy state.
progress-indeterminate · base base · style base-lyraOpen under the product themeContent guidelines
Name the task in label, not the widget: Uploading photos, never Progress.
The label is the accessible name read before the value. Prefer null to a
guessed number, because a bar parked at a made-up ninety promises an amount the
task has not reached.
Do
"Uploading photos" at a real 45. The label names the task and the readout matches the fill.
Don’t
A resting heart rate drawn as a fill towards a full track. It invents a target the reading never had.
Accessibility
Checked by pnpm run check:a11y on every commit. Colour is a role token
on neither axis; no type size is in px.
Audited against WCAG 2.2 AA, source and rendered, author-run rather than
independent, with clinical review still pending. Three fixes landed: a plain
aria-label on the root so the name survives a first paint before Base UI's
own wiring runs, a sibling status region announcing "{label} complete." at
max, and aria-hidden on the indeterminate branch's text to match the
determinate readout.
aria-valuenowcarries the amount done, with the label, minimum and maximum.- Nothing takes focus, so the bar costs no tab stop.
- The transition and pulse drop under
prefers-reduced-motion; words carry the state. - Type steps and the
emtrack height scale together, so 200% text grows the bar rather than clipping it.
Still open. Fill contrast against the track, carried below until measured. Wrap for a long label and print output were not checked.
| Key | Action | Notes |
|---|---|---|
| Tab | Skips the bar | Not a control. Nothing is focusable, so a keyboard user pays no tab stop. |
| Shift+Tab | Skips the bar | Same in reverse. The bar never appears in the focus order. |
| Arrow keys | Nothing | Nothing to select, move or change. The product sets the value. |
| Pair | Theme | APCA Lc | WCAG 2.2 | Floor |
|---|---|---|---|---|
| body text on the page | light | 101.6 | 17.18:1 | Pass |
| body text on the page | dark | -100.5 | 18.00:1 | Pass |
| body text on a card | light | 104.7 | 17.96:1 | Pass |
| body text on a card | dark | -99.6 | 16.32:1 | Pass |
| body text on the muted ground | light | 98.1 | 16.32:1 | Pass |
| body text on the muted ground | dark | -97.5 | 13.77:1 | Pass |
| secondary text on the page | light | 83.9 | 7.42:1 | Pass |
| secondary text on the page | dark | -80.7 | 13.44:1 | Pass |
| secondary text on a card | light | 87.0 | 7.76:1 | Pass |
| secondary text on a card | dark | -79.8 | 12.19:1 | Pass |
| secondary text on the muted ground | light | 80.4 | 7.05:1 | Pass |
| secondary text on the muted ground | dark | -77.8 | 10.28:1 | Pass |
| a hairline boundary on the page | light | 19.3 | 1.41:1 | Below floor |
| a hairline boundary on the page | dark | -8.2 | 1.90:1 | Below floor |
| a hairline boundary on a card | light | 22.4 | 1.47:1 | Below floor |
| a hairline boundary on a card | dark | -7.3 | 1.72:1 | Below floor |
| a hairline boundary on the muted ground | light | 15.8 | 1.34:1 | Below floor |
| a hairline boundary on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on the page | light | 69.8 | 4.52:1 | Pass |
| a placeholder boundary on the page | dark | -51.0 | 7.62:1 | Pass |
| a placeholder boundary on a card | light | 72.8 | 4.72:1 | Pass |
| a placeholder boundary on a card | dark | -50.1 | 6.91:1 | Pass |
| a placeholder boundary on the muted ground | light | 66.2 | 4.29:1 | Pass |
| a placeholder boundary on the muted ground | dark | -48.1 | 5.83:1 | Pass |
| a placeholder fill on the page | light | 8.3 | 1.18:1 | Below floor |
| a placeholder fill on the page | dark | -8.2 | 1.90:1 | Below floor |
| a placeholder fill on a card | light | 11.4 | 1.23:1 | Below floor |
| a placeholder fill on a card | dark | -7.3 | 1.72:1 | Below floor |
| a placeholder fill on the muted ground | light | 0.0 | 1.12:1 | Below floor |
| a placeholder fill on the muted ground | dark | 0.0 | 1.45:1 | Below floor |
| a placeholder boundary on its own fill | light | 58.9 | 3.84:1 | Pass |
| a placeholder boundary on its own fill | dark | -41.5 | 4.01:1 | Below floor |
| the card hairline on the page | light | 69.8 | 4.52:1 | Pass |
| the card hairline on the page | dark | -28.7 | 4.19:1 | Below floor |
| the card hairline on a card | light | 72.8 | 4.72:1 | Pass |
| the card hairline on a card | dark | -27.9 | 3.80:1 | Below floor |
These are the measured token pairs this component draws colour from, not a measurement of the component itself.
API reference
Prop
Type
Generated from ProgressProps in registry/bases/base/progress.tsx.
value={null} is a different state from value={0}: null draws the running
state and 0 a task not started. A value outside 0 to max is clamped by Base UI
and raises a development warning. No colour prop exists, and className takes
neither a status nor a category tint.