opsinjs
ComponentsFeedback

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

Theme
Density
Text
Status
progress · base base · style base-lyraOpen under the product theme

Installation

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-bar instead.
  • 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-dial instead.
  • 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 skeleton instead.

Anatomy

  1. ProgressThe root, a Base UI Progress.Root with role="progressbar" and the aria value attributes. Neutral chrome only, not focusable.Controlled by value
  2. Progress.LabelThe visible task name, wired by Base UI as the progressbar's accessible name. Comes from the required label prop.Controlled by label
  3. Progress.ValueThe text readout, formatted as a percentage. For an indeterminate task a plain phrase replaces it.
  4. Progress.TrackThe groove the fill runs in: the neutral muted surface, clipping its fill to a pill.
  5. 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.

Theme
Density
Text
Status
progress-a-task-completing · base base · style base-lyraOpen under the product theme

An 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.

Theme
Density
Text
Status
progress-indeterminate · base base · style base-lyraOpen under the product theme

Content 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-valuenow carries 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 em track 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.

KeyActionNotes
TabSkips the barNot a control. Nothing is focusable, so a keyboard user pays no tab stop.
Shift+TabSkips the barSame in reverse. The bar never appears in the focus order.
Arrow keysNothingNothing to select, move or change. The product sets the value.
PairThemeAPCA LcWCAG 2.2Floor
body text on the pagelight101.617.18:1Pass
body text on the pagedark-100.518.00:1Pass
body text on a cardlight104.717.96:1Pass
body text on a carddark-99.616.32:1Pass
body text on the muted groundlight98.116.32:1Pass
body text on the muted grounddark-97.513.77:1Pass
secondary text on the pagelight83.97.42:1Pass
secondary text on the pagedark-80.713.44:1Pass
secondary text on a cardlight87.07.76:1Pass
secondary text on a carddark-79.812.19:1Pass
secondary text on the muted groundlight80.47.05:1Pass
secondary text on the muted grounddark-77.810.28:1Pass
a hairline boundary on the pagelight19.31.41:1Below floor
a hairline boundary on the pagedark-8.21.90:1Below floor
a hairline boundary on a cardlight22.41.47:1Below floor
a hairline boundary on a carddark-7.31.72:1Below floor
a hairline boundary on the muted groundlight15.81.34:1Below floor
a hairline boundary on the muted grounddark0.01.45:1Below floor
a placeholder boundary on the pagelight69.84.52:1Pass
a placeholder boundary on the pagedark-51.07.62:1Pass
a placeholder boundary on a cardlight72.84.72:1Pass
a placeholder boundary on a carddark-50.16.91:1Pass
a placeholder boundary on the muted groundlight66.24.29:1Pass
a placeholder boundary on the muted grounddark-48.15.83:1Pass
a placeholder fill on the pagelight8.31.18:1Below floor
a placeholder fill on the pagedark-8.21.90:1Below floor
a placeholder fill on a cardlight11.41.23:1Below floor
a placeholder fill on a carddark-7.31.72:1Below floor
a placeholder fill on the muted groundlight0.01.12:1Below floor
a placeholder fill on the muted grounddark0.01.45:1Below floor
a placeholder boundary on its own filllight58.93.84:1Pass
a placeholder boundary on its own filldark-41.54.01:1Below floor
the card hairline on the pagelight69.84.52:1Pass
the card hairline on the pagedark-28.74.19:1Below floor
the card hairline on a cardlight72.84.72:1Pass
the card hairline on a carddark-27.93.80:1Below 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.

  • RangeBar draws a reading against a range somebody owns, with the reference bounds marked.
  • ScoreDial draws a composite score as an arc and says in words what it is made of.
  • Skeleton is the placeholder for arriving content whose shape you can show.

On this page