Skip to content
Monogem

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

Uploading files25%
Uploading files50%
Uploading files75%
Uploading files100%

Small

Indeterminate

Preparing your export…

Code Example

progress-showcase.tsx
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:

SizeHeightToken
default8px--scale-2
sm4px--scale-1

States

StateLookARIA
DeterminateIndicator width = value / max. Width transitions (--scale-free transition-[width], 300ms).aria-valuenow set
IndeterminateFixed 40%-wide indicator, gentle pulse (animate-pulse). Under prefers-reduced-motion the pulse is off — a static 40% bar.aria-valuenow omitted
Completevalue === 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.

TokenWhere used
--primaryIndicator fill
--mutedTrack fill — Color names this token for “subtle backgrounds”
--radius-fullTrack and indicator corners (pill)
--scale-2default height (8px)
--scale-1sm 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-label or aria-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-labelledby where 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-valuetext when 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" with aria-valuemin="0" and aria-valuemax always set. aria-valuenow is present only when determinate — its absence is the signal for “indeterminate,” so the component never emits a placeholder 0.
  • The component sets role and the aria-value* attributes after the caller’s props, so the progressbar contract can’t be half-overridden; aria-label, aria-labelledby, and aria-valuetext pass 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 a prefers-reduced-motion concern (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.

Related Components / Patterns

  • Skeleton — a placeholder while content loads.
  • Slider — the same track-and-fill language, for input.