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 updatearia-valuenowas 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 noaria-label. The role is announced but there is no name for the task.- Using
role="progressbar"for an indeterminate spinner AND settingaria-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>