Component · Navigation & Feedback
Progress
Overview
A horizontal bar that shows how far along a task is — a file upload, a multi-step form, a storage quota. Use it when the operation has a measurable completion point, or when it is running and the duration is unknown (the indeterminate state).
Anatomy
Track (full-width rounded rail) → indicator (fills from the left).
- Track — the full-width rounded rail.
- Indicator — the fill that grows from the left.
No baked-in label or percentage text — the caller supplies visible text alongside the bar and associates it (see Accessibility). Progress renders only the bar.
Live Example
Determinate
Small
Indeterminate
Code Example
import { Progress } from "@/components/ui/progress";
/**
* Live Progress example — Progress is a server component (no client JS). Shows
* determinate values, both sizes, and the indeterminate (pulsing, unknown)
* state. Each bar carries an accessible name via `aria-labelledby` pointing at
* a visible label — a progress bar with no name is a review finding.
*/
export function ProgressShowcase() {
return (
<div className="flex flex-col gap-8">
<section className="flex flex-col gap-4">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Determinate
</h3>
<div className="flex max-w-sm flex-col gap-4">
{[25, 50, 75, 100].map((v) => (
<div key={v} className="flex flex-col gap-1.5">
<span
id={`progress-${v}`}
className="flex justify-between text-xs leading-snug text-muted-foreground"
>
<span>Uploading files</span>
<span>{v}%</span>
</span>
<Progress value={v} aria-labelledby={`progress-${v}`} />
</div>
))}
</div>
</section>
<section className="flex flex-col gap-4">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Small
</h3>
<div className="max-w-sm">
<Progress
value={60}
size="sm"
aria-label="Storage used, 60 percent"
/>
</div>
</section>
<section className="flex flex-col gap-4">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Indeterminate
</h3>
<div className="flex max-w-sm flex-col gap-1.5">
<span
id="progress-indeterminate"
className="text-xs leading-snug text-muted-foreground"
>
Preparing your export…
</span>
<Progress indeterminate aria-labelledby="progress-indeterminate" />
</div>
</section>
</div>
);
}Variants
Monochrome by design. The indicator is --primary on a --muted track — there is no
success / warning / error colouring. A red progress bar would collide with the Color
foundation’s “red means danger, only”; a green one implies a result the bar hasn’t earned yet.
Completion is conveyed by the bar reaching 100% and the caller’s own text, not by a colour
change.
Two size steps:
| Size | Height | Token |
|---|---|---|
default | 8px | --scale-2 |
sm | 4px | --scale-1 |
States
| State | Look | ARIA |
|---|---|---|
| Determinate | Indicator width = value / max. Width transitions (--scale-free transition-[width], 300ms). | aria-valuenow set |
| Indeterminate | Fixed 40%-wide indicator, gentle pulse (animate-pulse). Under prefers-reduced-motion the pulse is off — a static 40% bar. | aria-valuenow omitted |
| Complete | value === max — a full bar. Not a distinct visual style; the caller decides whether to keep showing it, swap in a success Alert, etc. | aria-valuenow === aria-valuemax |
value is clamped to 0…max. max defaults to 100 and falls back to 100 if given as ≤ 0.
Usage Guidance
Tokens
Progress uses existing Monogem semantic and primitive tokens to stay consistent with the design system; it introduces no component-specific styling values.
| Token | Where used |
|---|---|
--primary | Indicator fill |
--muted | Track fill — Color names this token for “subtle backgrounds” |
--radius-full | Track and indicator corners (pill) |
--scale-2 | default height (8px) |
--scale-1 | sm height (4px) |
Indicator-on-track contrast is 16.4:1 (light) / 14.5:1 (dark) — comfortably past WCAG 1.4.11’s 3:1 for a meaningful UI graphic.
Do / Don’t
Do
- Give every Progress an accessible name with
aria-labeloraria-labelledby— a bar with no name tells a screen reader user nothing. - Show a visible label and/or percentage next to the bar; associate it with
aria-labelledbywhere practical so the visible text and the accessible name are the same string. - Use the indeterminate state while a task is genuinely running with no known progress, then switch to determinate as soon as you can report a number.
- Use
aria-valuetextwhen the meaningful unit isn’t a percentage (“Step 2 of 5”).
Don’t
- Colour the bar to signal success or failure — pair it with an Alert or status text instead.
- Use Progress for an indefinite spinner with no bar semantics — that’s a loading indicator, a different affordance.
- Animate a determinate bar backwards. If a value can drop, that’s a gauge, not progress.
- Leave an indeterminate bar on screen forever — it should resolve.
Accessibility
role="progressbar"witharia-valuemin="0"andaria-valuemaxalways set.aria-valuenowis present only when determinate — its absence is the signal for “indeterminate,” so the component never emits a placeholder0.- The component sets
roleand thearia-value*attributes after the caller’s props, so the progressbar contract can’t be half-overridden;aria-label,aria-labelledby, andaria-valuetextpass straight through. - Motion: the indeterminate pulse respects
prefers-reduced-motion(motion-reduce:animate-none). The determinate width transition is a short 300ms ease and is not aprefers-reduced-motionconcern (no looping, no large translation). - Don’t rely on the bar alone for a critical number — back it with text a screen reader reads in order.