Standards · ARIA

Role Widget

progressbar

Marks an element as a progress indicator. Use <progress> first — it ships the semantic, the determinate visual, and the value attributes natively. Reach for role="progressbar" when you must style beyond what the native element allows.

When to use

Use <progress value="42" max="100">. The native element is hard to style consistently across browsers, which is the main reason to reach for role="progressbar" on a custom <div> instead.

Two flavours:

  • Determinate — you know the total. Set aria-valuemin, aria-valuemax, and update aria-valuenow as work progresses.
  • Indeterminate — you do not know the total. Omit aria-valuenow (or do not set it). The screen reader announces “busy” / “loading” without a percentage.

Set aria-valuetext when the bare number is not meaningful — e.g. aria-valuetext="Uploading photo 3 of 8" reads better than “37 percent”.

A progressbar is NOT a live region — screen readers do not announce every aria-valuenow change. If you need the change to be spoken, also use role="status" on a companion element with the human-readable progress text.

Common failures

  • role="progressbar" on a spinner with no aria-label. The role is announced but there is no name for the task.
  • Using role="progressbar" for an indeterminate spinner AND setting aria-valuenow="0". That announces “0 percent complete” forever.
  • Forgetting to remove the progressbar (or its aria-busy) when work completes. Some screen readers continue polling.
  • Pairing a progressbar with a visually-hidden percentage but not updating either when the upload stalls. Users get a stuck state with no announcement.
  • Putting role="progressbar" on the wrapping container AND on the inner fill bar. Duplicate roles; pick one.

Example

<!-- Preferred -->
<label for="upload">Uploading</label>
<progress id="upload" value="42" max="100">42%</progress>

<!-- Custom determinate -->
<div id="uploadLabel">Uploading photo 3 of 8</div>
<div
  role="progressbar"
  aria-labelledby="uploadLabel"
  aria-valuemin="0"
  aria-valuemax="100"
  aria-valuenow="37"
></div>