Designer view
Progress
A non-interactive indicator showing the completion status of an ongoing task — uploads, downloads, multi-step server operations, long form submissions. Carries `role="progressbar"` plus an accessible name; determinate progress reports `aria-valuemin` / `aria-valuemax` / `aria-valuenow` so AT users hear the percentage; indeterminate progress omits `aria-valuenow` per APG and announces a busy state. Two structural variants (linear bar, circular ring) cover the canonical visual shapes; native HTML `<progress>` is an acceptable alternative implementation when no styling control is needed.
Also called Progress bar Progress indicator Loader
When to use
Use
For tasks with a measurable completion fraction (file upload showing 0-100%, multi-step server-side operation reporting step N of M) — render the determinate variant with `aria-valuenow` reporting the percentage. For tasks with no measurable fraction but where the user must know the system is working (server fetch with unknown latency, background sync) — render the indeterminate variant with a reduced-motion fallback. Linear variant for in-page progress that has horizontal space; circular variant for button-adjacent or compact contexts.
Avoid
For loading states where the eventual layout is known — that is `Skeleton` (preserves layout, no progress meaning). For transient post-completion notifications — that is `Toast` (system-pushed signal). For static completion messages once a task is done — that is `role="status"` or `<output>`, not `progressbar`. For multi-step user-driven workflows with state carried between steps — that is `Stepper`. For purely decorative spinning visuals not tied to a real task — render no progress affordance; loaders without backing operations confuse AT users about whether the system is working.
Versus related
- skeleton
`Skeleton` reserves layout while the eventual content is fetched — placeholder shapes mirror the final layout, no percentage. `Progress` reports the completion fraction (or busy state) of an ongoing task — no layout preservation, just status. Choose Skeleton when the layout is the message ("the page will look like this"); choose Progress when the completion is the message ("we are 60% through"). A single load may use both — Skeleton in the data region, Progress in a header showing overall completion across multiple loads.
- toast
`Toast` is a transient post-completion notification ("Upload complete"); `Progress` is the during-task indicator that the toast typically follows. Skeleton-during-load → Progress-while-working → Toast- on-complete is a canonical pairing across a long- running operation. Toast carries notification semantics (`role="status"` or `aria-live="polite"`); Progress carries progress semantics (`role="progressbar"` plus `aria-valuenow`).
Progress communicates "this is working" plus, when known, "this is how far along". The reference documents the `role="progressbar"` contract, the accessible-name requirement (`aria-label` or `aria-labelledby` — never unlabeled), the determinate-vs-indeterminate split (omit `aria-valuenow` for indeterminate per APG), the `prefers-reduced-motion` fallback for indeterminate animation (WCAG 2.3.3), and the boundary with `Skeleton` (use Skeleton when the eventual layout is preserved and progress is unmeasurable; use Progress when there is a measurable percentage or a busy state to surface).
Figma anatomy
| Slot | Figma type | Hint |
|---|---|---|
root | frame | Progressbar frame; variant property switches between linear and circular shapes |
track | instance | Track shape — bar (linear) or ring (circular); muted color |
fill | instance | Fill shape — bar (linear) or arc (circular); accent color; animation per variant |
label | text | Label text style; positioned above or beside the progress shape |
value-display | text | Numeric value text; aligned trailing (linear) or centered (circular) |
status-icon | instance | Icon instance — checkmark (complete) or x (error); aria-hidden |
helper-text | text | Helper text style; muted color; positioned below progress shape |
announcement | text | Visually-hidden text element for milestone announcements |
Token usage per slot
track- color
- background
color.surface.sunken
- background
fill- color
- background
color.accent.bg
- background
label- typography
- size
text.sm
- size
value-display- typography
- size
text.sm - weight
weight.semibold
- size
helper-text- color
- foreground
color.text.muted
- foreground
- typography
- size
text.sm
- size
Figma ↔ Code property map
| Figma | Kind | Code | Notes |
|---|---|---|---|
Variant | Enum | variant | Maps linear / circular structural shapes. |
Size | Enum | size | sm / md / lg. |
Determinate | Boolean | determinate | Positive-default boolean; true reports `aria-valuenow`, false omits it (indeterminate mode). |
Show Value | Boolean | showValue | Controls visibility of the value-display slot. AT announcement uses aria-valuenow regardless. |
Value | Number | value | 0..max; reflects to `aria-valuenow`. Required when determinate; ignored when indeterminate. |
Max | Number | max | Defaults to 100. Reflects to `aria-valuemax`. Custom max with `aria-valuetext` for non-percent contexts (steps, bytes). |
Aria Label | Text | ariaLabel | Accessible name. One of ariaLabel / ariaLabelledby is required. |
Aria Valuetext | Text | ariaValuetext | Optional human-readable value override for non-percent contexts ("Step 3 of 7", "2 MB of 5 MB"). |
Status | Enum | status | pending / inProgress / complete / error / indeterminate. Drives status-icon visibility and aria-valuetext. |
Label | Slot | label | Visible label slot; alternative to ariaLabel attribute. |
Helper Text | Slot | helperText | Optional secondary descriptive text below the progress shape. |
Motion
| Transition | Duration token |
|---|---|
valueTransition | motion.duration.fast |
indeterminateLoop | motion.duration.slower |
Responsive behaviour
| Breakpoint | Change |
|---|---|
breakpoint.sm | Below this width, the linear variant collapses to full-inline-width (loses any authored max- width); the circular variant scales to the container's `block-size`. Helper-text and value-display may stack below the progress shape rather than sitting beside it. |
breakpoint.md | At and above this width, both variants render at the authored size with helper-text and value-display in the canonical adjacent positions (linear: trailing; circular: centered inside the ring). |
Internationalisation
RTL · mirroring
Linear variant's fill direction follows the logical inline direction — fill grows from inline-start toward inline-end, which means visually right-to-left under RTL. CSS uses `inset-inline-start: 0` plus `inline-size: 60%` for the fill rather than `left: 0; width: 60%`. Circular variant's stroke direction is conventionally clockwise in LTR cultures; RTL implementations may keep clockwise (visually neutral, reads as "advancing") or flip to counterclockwise per locale convention. Document the chosen direction. Value-display number-formatting follows locale via `Intl.NumberFormat` (12.5% renders as 12,5 % in German). Accessible-name `aria-label` translates per locale.
Text expansion
Visible labels expand 30-50% under translation ("Loading" → "Wird geladen" 50% longer). Helper text expands similarly; reserve flexible inline- size in the helper-text slot. Value-display is stable across locales for percent values; non- percent contexts ("Step 3 of 7") expand ("Schritt 3 von 7" 30% longer) and may force the value-display below the bar at narrow widths. Long-running operations under expanded translations should favour the milestone-only announcement pattern over per-percent SR announcements.
Variants, properties, states
Variants
Structurally different versions of the component.
linear circular Properties
The same component, parameterised.
| Property | Type |
|---|---|
size | sm | md | lg |
determinate | boolean |
showValue | boolean |
States
Browser/user-driven (interactive) vs. app-driven (data).
| Kind | States |
|---|---|
interactive | |
data | pendinginProgresscompleteerrorindeterminate |
State transitions
| From | To | Trigger |
|---|---|---|
pending | inProgress | Consumer increments value above 0 |
inProgress | complete | Value reaches max (100%) |
inProgress | error | Underlying task fails; consumer sets the error data state |
inProgress | indeterminate | Consumer flips `determinate` from true to false (e.g., total size unknown mid-fetch) |
Figma↔Code mismatches
- 01 Figma
Inline "75%" text drawn as standalone label next to the bar
Code`aria-valuenow="75"` on the root plus optional `<value-display>` slot
ConsequenceDesigners compose progress with a percentage label drawn next to the bar; developers wire the same number as `aria-valuenow` on the root and may double-render it as visible text. The two surfaces diverge on AT — designer's standalone text is invisible to the progressbar's accessible value; developer's `aria-valuenow` is the canonical signal. Without explicit anatomy, the visible-versus-AT split is unclear and implementations may ship the visible text alone without setting `aria-valuenow`.
CorrectDocument the value-display slot as a visible mirror of the root's `aria-valuenow`. Figma carries the value text as a child of the progress instance, not a sibling. Code wires `aria-valuenow` on the root AND renders the value-display slot with `aria-hidden="true"` so the SR does not announce it twice.
- 02 Figma
Indeterminate drawn as decorative spinner without progress semantic
Code`role="progressbar"` without `aria-valuenow` plus reduced-motion fallback
ConsequenceDesigners compose indeterminate progress as a generic spinner-loading-icon that may map to Skeleton, Toast, or a bare decorative SVG; developers ship `role="progressbar"` so AT announces "progressbar, busy". The two surfaces diverge on what the system actually means — designer's "loading icon" reads as decorative, developer's "busy progressbar" reads as a real task. Without explicit anatomy, indeterminate progress may ship without the role at all and AT users hear no busy signal.
CorrectDocument indeterminate as a property of the progress component, not a separate decorative pattern. Figma carries an "indeterminate" variant of progress with the canonical animation and the `prefers-reduced-motion` fallback shape. Code ships `role="progressbar"` plus `aria-label`; omits `aria-valuenow` per APG; respects reduced-motion via CSS media query.
- 03 Figma
Circular ring drawn as decorative-only loading icon
Code`role="progressbar"` plus accessible name plus aria-valuetext for non-percent contexts
ConsequenceDesigners ship circular progress as an icon called "Loader" or "Spinner" with no progress semantic in the design surface; developers wire it as `role="progressbar"` because the operation has measurable progress. The two surfaces diverge on whether the affordance is progress or decoration — invisible distinction in the design file lets implementations ship either pattern, and AT users get the wrong announcement.
CorrectDocument circular as a structural variant of progress with the same role + accessible-name contract as linear. Figma carries Progress as one component with linear / circular variant property; "Spinner" components for purely decorative non-progress visuals are documented as a distinct pattern (or removed from the design system entirely if no real spinner- without-progress use-case exists).
- 04 Figma
Multi-step progress drawn as filled segments
CodeSingle progressbar with `aria-valuetext: "Step 3 of 7"` (or escalation to Stepper)
ConsequenceDesigners compose multi-step progress as a row of segments where each filled segment represents one completed step (e.g., 3 of 7 segments filled); developers ship either a single progressbar with `aria-valuetext` reporting the step or a Stepper component. The two patterns have different semantics — progressbar is continuous, stepper is sequential with state. Without explicit anatomy, design files may communicate progressbar visual while developers ship Stepper anatomy (or vice versa).
CorrectDocument the canonical decision rule — segments with no per-step state are progressbar with `aria-valuetext`; segments with per-step state (clickable, navigable, completed-vs-pending distinction) are Stepper. Figma carries two separate components and the design surface signals which semantic the implementation should adopt.
- 05 Figma
Progress drawn without label or value
Code`aria-label="Upload progress"` (or `aria-labelledby`) required on root
ConsequenceDesigners ship icon-only progress (compact bar with no surrounding text) for visual cleanliness; developers must add `aria-label` so AT users hear what the progress represents. The two surfaces diverge on accessibility — sighted users see a bar with no context (still bad UX), AT users hear nothing at all without the label.
CorrectDocument the accessible-name requirement as a non-negotiable contract. Figma carries an explicit label slot or documents the `aria-label` value as a component property (e.g., "Aria Label" property). Code rejects progress instances without `aria-label` or `aria-labelledby` — a build-time lint enforces the requirement.
Contracts
Non-negotiable contracts
WCAGWCAG 4.1.2 Name, Role, Value The progressbar element carries an accessible name via `aria-label` or `aria-labelledby`. Implementations that ship unlabeled progressbars violate WCAG 4.1.2 — AT users hear the percentage with no semantic context.
Without a label, SR users hear "progressbar, 43 percent" with no indication of what is at 43 percent. The percentage alone communicates nothing — uploads, downloads, syncs, and form-saves all sound identical. The user cannot tell which task is progressing or whether multiple progressbars are reporting different operations.
APGWAI-ARIA progressbar role + aria-valuenow guidance Determinate progress reports `aria-valuemin`, `aria-valuemax`, and `aria-valuenow`. Indeterminate progress OMITS `aria-valuenow` entirely (does not set it to 0 or the last known value).
Without the omit-rule, indeterminate progress announces a stale value ("0 percent" or "43 percent") while the actual state is "we don't know". SR users mistrust the value or misread the operation as stuck. The omit rule is what canonical APG-compliant progressbars do; the absence of `aria-valuenow` is the busy-state signal.
WCAGWCAG 2.3.3 Animation from Interactions Indeterminate animation respects `prefers-reduced-motion: reduce` — the canonical fallback is a static busy indicator (stripe pattern, single-frame pulse, or non-animated shape with `aria-valuetext="Working"`).
Without the fallback, users with vestibular sensitivity or animation-fatigue experience sustained discomfort during long operations. WCAG 2.3.3 is AAA but the canonical contract treats it as required because the cost of compliance is one CSS media query.
HTML specHTML standard — progress element semantics The progressbar element is non-interactive — no `tabindex`, no role conflicts. The component does not participate in tab-order and does not respond to keyboard or pointer activation.
Without the rule, implementations may add `tabindex="0"` to draw focus on completion or `onClick` to act as cancel-buttons. Both patterns confuse keyboard users who encounter unexpected tab stops on what should be observation-only surfaces. Cancel affordances live as sibling elements, not on the progressbar itself.
HTML specHTML standard — progress element semantics Native `<progress>` is an acceptable alternative implementation when no styling control is required. It carries the role + value semantics natively and respects `prefers-reduced-motion` automatically in browsers that animate the indeterminate state. Custom-styled `role="progressbar"` is preferred when the design system needs explicit control over the visual shape.
Without documenting the alternative, implementations may unnecessarily reinvent the role-driven pattern when native `<progress>` would suffice. Native `<progress>` styling is limited (browser-specific pseudo-elements: `::-webkit-progress-bar`, `::-moz-progress-bar`); design systems with bespoke progress visuals choose `role="progressbar"`.
Vocabulary drift
- WAI-ARIA
role=progressbar + aria-valuemin/max/now- Canonical ARIA pattern. WAI-ARIA Authoring Practices Guide documents the omit- `aria-valuenow`-for-indeterminate rule explicitly; the canonical anatomy mirrors this discipline.
- HTML
<progress>- Native HTML element with `value` and `max` attributes; implies `role="progressbar"`. No native circular variant — design systems implement circular via SVG + `role="progressbar"`. Native `<progress>` animation for indeterminate state is browser-specific.
- Material 3
LinearProgressIndicator + CircularProgressIndicator- Material 3 ships linear and circular as separate components rather than a single component with variants. Both carry the progressbar semantic; the canonical single-component-with-variants reflects the ARIA pattern more directly than the Material split.
- Carbon
ProgressBar + InlineLoading + Loading- Carbon ships three patterns — `ProgressBar` (determinate linear), `InlineLoading` (status-text-with-spinner for inline contexts), `Loading` (full-page circular spinner). The canonical single-component covers all three via variant + status property.
- Atlassian
ProgressBar + Spinner- Atlassian splits into ProgressBar (determinate linear) and Spinner (indeterminate circular). The canonical single-component with linear / circular variants plus determinate boolean covers both.
- Polaris
ProgressBar + Spinner- Polaris similarly splits. ProgressBar is determinate linear; Spinner is indeterminate circular. Mobile-first Polaris guidance favours Spinner over ProgressBar for short operations and ProgressBar for long determinate ones.
- React Aria
ProgressBar (with isIndeterminate)- React Aria ships a single ProgressBar primitive with `isIndeterminate` prop, matching the canonical single-component shape. Headless — consumers compose the visual track + fill themselves via the `useProgressBar` hook.
- GitHub Primer
ProgressBar- Primer ships a linear-only ProgressBar with `progress` (0-100), `bg`, `barSize` props. No circular variant; Primer recommends external spinner libraries for indeterminate circular contexts.
Common mistakes
#progress-no-label
Progress has no `aria-label` or `aria-labelledby`
The progressbar ships without an accessible name. SR users hear "progressbar, 43 percent" with no indication of what is at 43 percent — upload? download? form save? The percentage alone communicates nothing semantically. WCAG 4.1.2 Name-Role-Value violation.
Set `aria-label="Upload progress"` (or the task-specific name) on the root, or `aria-labelledby` pointing to a sibling label element's id. The label must name what is in progress — "Loading", "Working", or generic names pass the lint but fail the user. Translate the label per locale.
#progress-indeterminate-with-valuenow
Indeterminate progress carries `aria-valuenow`
Indeterminate progress (no measurable percentage) ships with a stale `aria-valuenow` (often `0` or the last known value before the fetch became indeterminate). SR users hear "progressbar, 0 percent" or "43 percent" while the actual state is "we don't know". APG explicit rule violation.
Remove `aria-valuenow` entirely when `determinate=false`. Keep `aria-valuemin`, `aria-valuemax`, and the accessible name. SR announces "progressbar, busy" — the canonical indeterminate signal. Set or unset `aria-valuenow` on the same element as `determinate` flips; do not let the attribute lag.
#progress-no-reduced-motion
Indeterminate animation does not honor `prefers-reduced-motion`
Indeterminate progress animates continuously (rotating ring, sliding bar, pulsing fill) under `prefers-reduced-motion: reduce`. Users with vestibular sensitivity or who fatigue from animation experience visual stress; on long operations the unstoppable motion becomes a real barrier. WCAG 2.3.3 Animation from Interactions violation.
Wrap the indeterminate animation in `@media (prefers-reduced-motion: reduce)` and fall back to a non-animated busy indicator — a static stripe pattern, a single-frame pulse, or simply the progress shape with `aria-valuetext="Working"` and no continuous motion. The fallback must still communicate "in progress" visually for sighted users.
#progress-as-status-message
Progress component used for completion notification
The progress shape stays on screen after the task completes, sometimes with a checkmark replacing the fill, as a "complete" status message. SR users continue hearing "progressbar" semantics for what is now a static state — the role contradicts the meaning. Common in implementations that reuse the Progress component for both during-task and post-task surfaces.
On task completion, transition the surface to a different component — Toast for transient notification, `role="status"` text for inline confirmation, or `<output>` for form-result announcements. The Progress component's lifetime is during-task only; it unmounts or transitions out on completion. Implementations that show a "complete" checkmark inside progress for one second before transitioning are acceptable as long as the role-vs-meaning contradiction is brief.
#progress-not-progressbar-role
Visual progress without `role="progressbar"`
The component renders a visible progress bar (or ring) but the underlying DOM is a plain `<div>` with no role. AT users hear nothing — no progressbar role, no value, no busy state. The affordance functions visually for sighted users and is invisible to everyone else.
Add `role="progressbar"` (or use native `<progress>`) plus `aria-valuemin`, `aria-valuemax`, `aria-valuenow` (determinate), `aria-label` or `aria-labelledby`. The canonical anatomy carries the role on the root slot; implementations must wire it.
Accessibility hints
| Slot | Accessibility hint | |
|---|---|---|
root | `<div role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="60" aria-label="Upload progress">` (determinate) or `<div role="progressbar" aria-label="Loading">` without `aria-valuenow` (indeterminate). Accessible name is REQUIRED — `aria-label` or `aria-labelledby` pointing to a sibling label element. Native `<progress value="60" max="100">` is the alternate-implementation; both surfaces are canonical, the choice is implementation discretion. | |
track | `aria-hidden="true"` is canonical. Track is visual scaffolding only; AT reads the progressbar's `aria-valuenow` via the root, not the SVG / DOM shapes. SVG implementations should mark inner `<circle>` / `<rect>` shapes with `aria-hidden` explicitly to prevent SR drift on raw shape announcements. | |
fill | `aria-hidden="true"`. The fill is the visible progress; the AT semantic lives on the root. Indeterminate fill animates continuously when `prefers-reduced-motion` is `no-preference`; under `reduce`, the fill renders as a static busy indicator (e.g., a pulsing-once or a stripe- pattern) without continuous motion. | |
label | Render as `<span id="progress-label">` and wire `<div role="progressbar" aria-labelledby="progress-label">`. The label and value-display together compose the SR announcement ("Uploading file 3 of 7, progressbar, 43 percent"). Avoid duplicating the label as both `aria-labelledby` and visible text (works) versus only `aria-label` (works but sighted users see no label) — pick one canonical pattern per design system. | |
value-display | Decorative for AT (`aria-hidden="true"`) when the root's `aria-valuenow` reports the same number; the SR otherwise announces the value twice. When the visible text differs from the numeric value (e.g., "Step 3 of 7"), set the root's `aria-valuetext` to the same string and keep the slot decorative — `aria-valuetext` overrides `aria-valuenow` in the SR announcement. | |
status-icon | `aria-hidden="true"`. The icon is visual confirmation of the terminal state; the AT semantic lives on the root via `aria-valuetext="Complete"` (or "Failed"). Do not give the icon its own role or accessible name — this duplicates the announcement and produces "Complete, image, complete" patterns on some SR / browser combinations. | |
helper-text | Render as `<span id="progress-helper">` and wire `<div role="progressbar" aria-describedby="progress-helper">` so SR users hear the helper text alongside the value announcement. Avoid time-based predictions the user cannot verify ("2 minutes remaining" when the actual ETA fluctuates wildly is more confusing than no estimate). | |
announcement | `<span aria-live="polite" class="sr-only">` whose text content updates at canonical milestones ("25 percent complete", "50 percent complete", ...). Use `aria-live="polite"` not "assertive" — progress is non-urgent; assertive interrupts SR users in mid-sentence. Long-running operations benefit from milestone announcements; short operations (under 5 seconds) usually do not. |