For most React interfaces, start with the native <progress> element and style it. It already communicates progress to assistive technology and supports both known and unknown completion amounts. Build a custom element with role="progressbar" only when native markup cannot meet the visual or DOM requirement—and then implement its accessibility details yourself.
Start with native <progress> when it fits
The native element is the simplest reusable option. In React, its value is a number from zero through max; max defaults to 1, so set it explicitly when your component uses percentages. React treats value={null} as indeterminate progress. [React]
function ProgressBar({ value, label = "Progress" }) {
const indeterminate = value == null;
return (
<label className="progress">
<span className="progress__label">{label}</span>
<progress
className="progress__track"
value={indeterminate ? undefined : value}
max={100}
aria-label={label}
/>
{!indeterminate && <span>{value}%</span>}
</label>
);
}
This is a starting point, not a complete input policy. Decide what the component should do with values below zero or above 100, and whether the displayed percentage should be rounded. Keep the visible wording and accessible name clear in the consuming layout; in this example, both are provided, so avoid adding another competing label around the component.
The native element has a minimum of zero and an implicit progressbar role. Its max must be greater than zero. Text placed between the opening and closing <progress> tags is fallback content, not the accessible label; provide a name separately. Browser styling varies, so style the element where supported and check the result in the browsers your interface targets. [MDN: <progress>]
Recommended Free Tools
#1 Best Overall
Choose the implementation that matches the requirement
| Option | Best fit | What you take on |
|---|---|---|
Styled native <progress> |
The desired design can be achieved with the native element. | Provide an accessible label; browser styling differs. Range and indeterminate behavior are built in. [MDN] |
Custom element with role="progressbar" |
Your DOM or rendering needs cannot be met adequately with <progress>. |
Implement the accessible name, range, current value or indeterminate state, and visual updates. The ARIA role alone does not give a generic element native behavior. [MDN: progressbar role] |
| React Aria ProgressBar | You want a documented library component with richer behavior. | Review its dependency cost and API fit; its documentation describes determinate and indeterminate progress and locale-aware value formatting. [React Aria] |
Use the native semantic element when it meets the requirement. A generic element gives more direct control over the markup, but it also makes range semantics and state management your responsibility. A progress bar represents completion of a task; it is not a gauge for a measurement such as disk space or a query’s relevance. [MDN: <progress>]
Build custom ARIA markup only when necessary
For custom markup, put role="progressbar" on the semantic wrapper. Give it an accessible name using aria-labelledby to reference a visible label outside the wrapper, or use aria-label. Descendants of a progressbar are treated as presentational, so do not put essential label text inside it. [MDN: progressbar role]
function CustomProgressBar({ value, label }) {
const indeterminate = value == null;
return (
<div>
<span id="upload-label">{label}</span>
<div
className="progress"
role="progressbar"
aria-labelledby="upload-label"
aria-valuemin={0}
aria-valuemax={100}
aria-valuenow={indeterminate ? undefined : value}
>
<div className="track">
<div
className="fill"
style={{ width: indeterminate ? "35%" : `${value}%` }}
/>
</div>
</div>
</div>
);
}
The example assumes that determinate values have already been validated to fall from zero through 100; add that validation or clamping before rendering. If your range is not zero through 100, expose the actual minimum and maximum and keep the current value within them. When users need a spoken value other than the numeric one—for example, a meaningful unit or stage—provide aria-valuetext as appropriate. [MDN: progressbar role]
Represent unknown progress honestly
When completion cannot be expressed as a known number, omit aria-valuenow and do not show a fabricated percentage. The animated fill in a custom indeterminate bar is only a visual activity cue; its apparent width must not suggest an exact completion amount. With native markup, omit the value attribute; in React, use value={null}. [React] [MDN: <progress>] [MDN: progressbar role]
Free tools Windows power users keep installed
One-click scans. No signup required.
Rank #3
Connect progress to the region it describes
If the bar describes a page region being updated, connect the indicator and region: set aria-describedby on the region to reference the progress indicator, and set aria-busy="true" on the region while the update is underway. Clear the busy state when the update finishes. [MDN: <progress>]
Quick Recap
Best Value
Rank #4
Check these details before using the component
- Give the indicator a concise accessible name, such as “Uploading report.”
- Use a determinate value only when the completed amount is known; otherwise expose an indeterminate state.
- Keep determinate values inside the component’s chosen minimum and maximum.
- For custom ARIA markup, keep essential label text outside the progressbar and reference it with
aria-labelledby. - Use a progress bar for task completion, not for a gauge or general measurement.
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.




