---
title: "Progress"
description: "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."
url: "https://opsinjs.pensievelabs.org/components/progress"
source: "https://opsinjs.pensievelabs.org/components/progress.md"
section: "Components"
status: "shipped"
kind: "component"
category: "feedback"
reviewed: "2026-09-20"
reviewer: "engineering"
aliases: ["progress bar", "completion"]
implemented: true
---

> Elements written as `<PascalCase … />` below are opsinjs documentation
> components. Their attributes are the content: the values they render are
> generated from `tokens/*.json` and `registry/catalogue.ts` and are
> published separately at https://opsinjs.pensievelabs.org/r/index.json and under the Reference
> section.
> `<StubNotice>` IS THE EXCEPTION, AND IT IS THE ONE TO READ. It is a
> paired element rather than a self-closing one, and the text between
> its opening and closing tags is prose an author wrote, reproduced
> below word for word. That prose is where this page says whether the
> component has been reviewed. Read the children, not only the
> attributes.

<StubNotice
  name="progress"
  status="shipped"
  questions="[
  &#x22;Nobody has listened to the bar in a screen reader, so whether label, then value, then busy state lands as a hearing reader expects is reasoned, not heard.&#x22;,
  &#x22;Under prefers-reduced-motion the indeterminate pulse is dropped, and whether a full static fill reads as running rather than done has not been checked with a reader who turns motion off.&#x22;,
  &#x22;Under forced colours the muted track and the primary fill are both replaced by the platform's pair, and whether the fill stays distinct from the track is untested.&#x22;,
]"
>
  Audited against WCAG 2.2 AA. Clinical review pending.
</StubNotice>

## Preview [#preview]

<ComponentPreview name="progress" />

## Installation [#installation]

<ComponentInstall name="progress" unbuilt="false" importPath="@/components/ui/progress" dependencies="[&#x22;@base-ui/react&#x22;]" />

## Usage [#usage]

```tsx
import { Progress } from "@/components/ui/progress"
```

```tsx
<Progress label="Uploading photos" value={45} />
```

## When to use it [#when-to-use-it]

<WhenToUse
  use="[
  &#x22;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.&#x22;,
  &#x22;Tracking a multi-step flow, such as step three of five, by passing the step count as value and the total as max.&#x22;,
  &#x22;Signalling that a task with no known shape is running, by passing value={null}, rather than inventing a percentage.&#x22;,
]"
  avoid="[
  { case: &#x22;You are drawing a health reading against a range. A bar implies a target the reading does not have and carries no reference bounds.&#x22;, instead: &#x22;range-bar&#x22; },
  { case: &#x22;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.&#x22;, instead: &#x22;score-dial&#x22; },
  { case: &#x22;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.&#x22;, instead: &#x22;skeleton&#x22; },
]"
/>

## Anatomy [#anatomy]

<Anatomy
  name="progress"
  parts="[
  {
    name: &#x22;Progress&#x22;,
    describes: &#x22;The root, a Base UI Progress.Root with role=\&#x22;progressbar\&#x22; and the aria value attributes. Neutral chrome only, not focusable.&#x22;,
    prop: &#x22;value&#x22;,
  },
  {
    name: &#x22;Progress.Label&#x22;,
    describes: &#x22;The visible task name, wired by Base UI as the progressbar's accessible name. Comes from the required label prop.&#x22;,
    prop: &#x22;label&#x22;,
  },
  {
    name: &#x22;Progress.Value&#x22;,
    describes: &#x22;The text readout, formatted as a percentage. For an indeterminate task a plain phrase replaces it.&#x22;,
  },
  {
    name: &#x22;Progress.Track&#x22;,
    describes: &#x22;The groove the fill runs in: the neutral muted surface, clipping its fill to a pill.&#x22;,
  },
  {
    name: &#x22;Progress.Indicator&#x22;,
    describes: &#x22;The fill, in the primary action role. Its width follows the value, or spans the track and pulses when indeterminate.&#x22;,
  },
]"
/>

<CompositionTree
  name="progress"
  tree="[
  {
    part: &#x22;Progress&#x22;,
    cardinality: &#x22;1&#x22;,
    note: &#x22;data-slot=\&#x22;progress\&#x22;, role=\&#x22;progressbar\&#x22;&#x22;,
    children: [
      {
        part: &#x22;Progress.Label&#x22;,
        cardinality: &#x22;1&#x22;,
        note: &#x22;data-slot=\&#x22;progress-label\&#x22;, the accessible name&#x22;,
      },
      {
        part: &#x22;Progress.Value&#x22;,
        cardinality: &#x22;1&#x22;,
        note: &#x22;data-slot=\&#x22;progress-value\&#x22;, percentage or phrase&#x22;,
      },
      {
        part: &#x22;Progress.Track&#x22;,
        cardinality: &#x22;1&#x22;,
        note: &#x22;data-slot=\&#x22;progress-track\&#x22;, the muted groove&#x22;,
        children: [
          {
            part: &#x22;Progress.Indicator&#x22;,
            cardinality: &#x22;1&#x22;,
            note: &#x22;data-slot=\&#x22;progress-indicator\&#x22;, the primary fill&#x22;,
          },
        ],
      },
    ],
  },
]"
/>

## Examples [#examples]

### A task completing [#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.

<ComponentPreview name="progress-a-task-completing" kind="example" align="start" />

### An indeterminate task [#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.

<ComponentPreview name="progress-indeterminate" kind="example" align="start" />

## Content guidelines [#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.

<DoDont>
  <DoDont.Do>
    **"Uploading photos" at a real 45.** The label names the task and the readout
    matches the fill.
  </DoDont.Do>

  <DoDont.Dont>
    **A resting heart rate drawn as a fill towards a full track.** It invents a
    target the reading never had.
  </DoDont.Dont>
</DoDont>

## Accessibility [#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.

<KeyboardTable
  name="progress"
  rows="[
  {
    keys: &#x22;Tab&#x22;,
    action: &#x22;Skips the bar&#x22;,
    notes: &#x22;Not a control. Nothing is focusable, so a keyboard user pays no tab stop.&#x22;,
  },
  {
    keys: &#x22;Shift + Tab&#x22;,
    action: &#x22;Skips the bar&#x22;,
    notes: &#x22;Same in reverse. The bar never appears in the focus order.&#x22;,
  },
  {
    keys: &#x22;Arrow keys&#x22;,
    action: &#x22;Nothing&#x22;,
    notes: &#x22;Nothing to select, move or change. The product sets the value.&#x22;,
  },
]"
/>

<ContrastReport component="progress" />

## API reference [#api-reference]

<PropsTable name="ProgressProps" />

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

## Related [#related]

* [RangeBar](./range-bar.mdx) draws a reading against a range somebody owns, with the reference bounds marked.
* [ScoreDial](./score-dial.mdx) draws a composite score as an arc and says in words what it is made of.
* [Skeleton](./skeleton.mdx) is the placeholder for arriving content whose shape you can show.
