Skip to content

Progress Bar

A determinate progress indicator built on the role="progressbar" pattern. Communicates task completion to all users — including those using assistive technology.

The ProgressBar communicates the status of a long-running, measurable process. It implements the WAI-ARIA progressbar pattern with the right ARIA attributes, clamps out-of-range values, and supports four semantic colour variants paired with a visible label.

When to Use

  • You can measure progress. File uploads, multi-step forms, batch jobs, scan progress — anywhere you know the current value and total.
  • The wait is more than ~1 second. Below that, a brief loading state is enough — a progress bar that flashes by is more distracting than helpful.
  • You want to set expectations. A determinate bar tells users not just that something is happening, but how much longer they'll wait.

If progress can't be measured, use a spinner with role="status" and a polite live region instead — see Indeterminate Progress below. For initial page or content load, prefer a Skeleton.

Accessibility Features

  • Semantic role: Uses role="progressbar" with aria-valuenow, aria-valuemin, and aria-valuemax — meets WCAG 4.1.2 (Name, Role, Value).
  • Required label: The label prop is required and surfaces both visually and as aria-label. Satisfies WCAG 1.3.1 (Info and Relationships).
  • Percentage announcement: Screen readers compute the percentage from valuenow/min/max and announce it on update.
  • Custom value text: Use valueText (mapped to aria-valuetext) when "73%" is less helpful than "Step 3 of 5" or "73 of 100 files uploaded".
  • Not colour-only: Variants (success/warning/danger) reinforce status, but the visible label always carries the meaning — meets WCAG 1.4.1 (Use of Color).
  • Out-of-range safe: Values outside [min, max] are clamped, so the bar never overflows visually or sends invalid ARIA values.

Usage Examples

Basic upload

import { ProgressBar } from '@holmdigital/components'; <ProgressBar value={65} label="Uploading file..." showValueLabel />

Multi-step form (custom range + valueText)

<ProgressBar value={3} min={0} max={5} label="Form progress" valueText="Step 3 of 5: Payment" variant="success" showValueLabel />

The visible label says "Form progress" while screen readers announce the more informative "Step 3 of 5: Payment".

Reflecting state with variants

function ScanProgress({ value, status }) { const variant = status === 'failed' ? 'danger' : status === 'warning' ? 'warning' : status === 'done' ? 'success' : 'primary'; return ( <ProgressBar value={value} label={`Accessibility scan: ${status}`} variant={variant} showValueLabel /> ); }

Indeterminate Progress

This component is determinate only. If you don't know how long the operation will take, do not pass a fake value (such as a spinning 0 %). Use a status region instead.
<div role="status" aria-live="polite" className="flex items-center gap-2"> <Spinner aria-hidden="true" /> <span>Loading results…</span> </div>

When the operation finishes, replace the contents of the same role="status" region with the result message — screen readers will announce it politely without stealing focus. This pattern aligns with WCAG 4.1.3 (Status Messages).

Live Updates & Screen Readers

Screen readers announce aria-valuenow changes on a progressbar automatically, but they vary in how often. A few practical guidelines:

  • Throttle rapid updates. If your progress updates 30+ times per second, batch them — typical guidance is to update no more than every 250–500 ms.
  • Announce milestones, not noise. For long jobs, consider a separate polite live region for "0 % → 25 % → 50 % → 75 % → done" while the bar itself updates smoothly.
  • Don't move focus on completion. When the task finishes, update text instead of moving focus — moving focus during background work disrupts what the user is doing.

Props

Props Reference

PropTypeDefaultDescription
value *number-Current progress value. Clamped to [min, max].
label *string-Accessible label. Rendered visibly above the bar AND set as aria-label.
min number0Minimum value (aria-valuemin).
max number100Maximum value (aria-valuemax).
showValueLabel booleanfalseRenders a visible percentage next to the label (e.g. "65 %").
valueText string-Overrides the screen reader's value announcement via aria-valuetext (e.g. 'Step 3 of 5').
variant 'primary' | 'success' | 'warning' | 'danger''primary'Color variant. Pair with a meaningful label — colour alone is not enough (WCAG 1.4.1).
className string-Additional CSS classes for the wrapper element.

Best Practices

  • Always provide a meaningful label. "Loading" is not enough — say what is loading. The label is required and is what assistive tech reads first.
  • Use valueText when units matter. "47 of 200 files" or "Step 3 of 5" tells the user something a percentage doesn't.
  • Match variant to status, not aesthetic. danger means "something went wrong"; reserve it for real failure states.
  • Show the visible percentage when it adds value. For short tasks (under ~5 s) the bar itself is enough; for long ones, showValueLabel reassures sighted users that work is still happening.
  • Pair with a separate error message on failure. A red bar at 60 % does not explain what went wrong — surface the reason via a status message or alert.

Common Mistakes

Using a progress bar for indeterminate loading. Animating from 0 → 100 → 0 with no real value misleads users and confuses screen readers. Use a status region with a spinner instead.
Relying on colour for state. A red bar means nothing to colour-blind users without an accompanying word like "Failed" or "Error".
Missing or generic label. Without a real label, screen reader users hear "progress bar, 65 percent" — 65 % of what?
Updating value 60 times per second. The visible animation is smooth, but rapid aria-valuenow changes can spam screen reader announcements. Throttle to ~250 ms.