Skip to documentation content

Progress

Show how much of a task is complete or that work is still in progress.

progress-demo

Overview

Use Progress to show how far an active task has advanced: uploads, imports, onboarding steps, generation, installation. The defining property of a progress bar is that its endpoint moves — the value climbs while work runs and stops when the task finishes. For quantities that simply are a certain amount right now, such as storage used or budget spent, use Meter instead.

Anatomy

A progress indicator has a root carrying the value semantics, an optional label naming the operation, an optional formatted value, and a visual bar or ring. With no children, Progress renders the matching track for its variant automatically; circular layouts also center ProgressValue inside the ring for you.

Behavior

Pass a number as soon as the application can genuinely estimate progress, and pass value={null} only when it cannot — an indeterminate bar says "working, duration unknown", which is honest. Switching from indeterminate to determinate mid-task is fine and often ideal: show the spinner-like state during connection, then real percentages once bytes are counted.

Never animate a made-up percentage just so the interface feels alive. A bar that creeps to 90% and sits there teaches people your numbers mean nothing. If the estimate stalls, say so in surrounding text rather than faking motion.

Accessibility

The root emits role="progressbar" with aria-valuemin, aria-valuemax, and aria-valuenow. Screen readers announce the accessible name plus the current value against the range — for example "Importing contacts, 40%". Give the operation its name through ProgressLabel (wired to the root automatically) or aria-label when space is tight.

When the raw percentage is unclear, use getAriaValueText or format to control what gets announced — "2.1 of 4 GB transferred" beats "40" for uploads. Render ProgressValue when sighted users also need the number; do not make anyone derive meaning from bar length alone.

Indeterminate animation runs only when motion is safe: under reduced-motion preferences the linear bar becomes a static, dimmed partial fill and the ring holds still, so nothing spins uncontrollably. Colors come from theme tokens for both themes. When the task completes, report it in surrounding content — a status line, a toast, new content appearing — because the bar itself disappears and proves nothing.

Installation

npx honestui@latest add progress

Usage

import {
  Progress,
  ProgressLabel,
  ProgressValue,
} from "@/components/ui/progress";
<Progress value={40} />

Note: If you render children inside Progress, you must also include ProgressTrack and ProgressIndicator inside it. Without them, the bar will not display. When no children are provided, a default track and indicator are rendered for you.

Don't do this

Fake progress

// Bad
const [value, setValue] = useState(0);
useEffect(() => {
  const t = setInterval(() => setValue((v) => Math.min(v + 5, 95)), 500);
  return () => clearInterval(t);
}, []);
return <Progress value={value} />;
// Good
<Progress value={uploadProgress ?? null} />
// uploadProgress comes from real transfer events; null while unknown

An invented ticker produces the worst possible outcome: a bar that reaches 90% on a task that then fails, or completes instantly after crawling. People learn to ignore the component entirely. Use value={null} while the outcome is genuinely unknown and switch to real numbers when events arrive.

An unnamed bar

// Bad
<Progress value={40} />
// Good
<Progress value={40}>
  <div className="flex justify-between">
    <ProgressLabel>Importing contacts</ProgressLabel>
    <ProgressValue />
  </div>
  <ProgressTrack>
    <ProgressIndicator />
  </ProgressTrack>
</Progress>

Without a label, assistive technology announces "40%" with no idea what it belongs to — on a page with two operations running, that announces nothing useful. Name every progress bar, even single ones; screens change.

Progress where Meter belongs

// Bad
<Progress value={72}>
  <ProgressLabel>Storage used</ProgressLabel>
</Progress>
// Good
<Meter value={72}>
  <MeterLabel>Storage used</MeterLabel>
  <MeterValue />
</Meter>

Storage does not advance toward completion; it sits at a measured level within a capacity. Announcing role="progressbar" invites screen-reader users to wait for a finish that never comes. Reserve Progress for tasks that end.

Examples

Onboarding steps

A labeled determinate bar tracking multi-step setup.

progress-onboarding

With Label and Value

progress-with-label-value

With Formatted Value

Custom units and precision via the formatting props.

progress-with-formatted-value

API reference

Progress accepts all Base UI Progress root props plus Honest UI's variant:

PropValuesDefault
variantlinear, circularlinear
valuenumber or null (indeterminate)required
min / maxnumber0 / 100

The root carries role="progressbar" with aria-valuemin, aria-valuemax, aria-valuenow, and aria-valuetext; format (Intl options), locale, and getAriaValueText(formattedValue, value) control the announced text. Current status (indeterminate, progressing, complete) is exposed as data attributes for styling. Computed percentages are clamped to 0–100.

Parts: ProgressLabel, ProgressTrack, ProgressIndicator, and ProgressValue forward their Base UI props; ProgressCircularTrack accepts native SVG props.

See the Base UI Progress API for value formatting and state details.