Search topics
Progress bars and meters that screen readers can't read
An upload bar is drawn with styled divs, or given a progressbar role with no name and a value that never moves, so a screen reader hears nothing of how far along it is or when it ends.
<div class="bar"><div class="fill" style="width: 60%"></div></div> The problem
Screen reader users start an upload and can't tell how far along it is, whether it is still going, or when it has finished.
A support form lets you attach a file. You choose report.pdf and press Upload. Under the file name, a grey bar fills with blue over a few seconds, and when it is full, a tick and "Uploaded" appear. A sighted user watches it fill and waits. A screen reader user presses Upload and hears nothing. If they go looking, the bar is either not there at all, or it reads "progress bar, 0%" while it is drawn more than half full. They can't tell whether the upload started, how long it has left or whether it finished, so they press Upload again, or send the form before the file is attached.
Two things are missing. The first is the bar itself. In one form of this failure it is two div elements, a track and a fill whose width the script changes, so it has no role, no name and no value: to assistive technology there is nothing there. In the other, someone added role="progressbar", but with no name, and the script moves the fill without touching aria-valuenow, so a screen reader reads 0% to the end. Success Criterion 4.1.2 asks that components generated by script expose their name and role, and that changes to their values are available to assistive technology. Whether a bar that only shows a value counts as a "user interface component" is open to argument, and axe files an unnamed progress bar under 1.1.1 Non-text Content instead. Either way, the bar's meaning has to reach assistive technology, and the fixes are the same.
The second is the news. "Half done" and "Uploaded" are status messages: they report the progress and result of a process without taking focus, which is what 4.1.3 covers, and its Understanding page gives a progress bar with occasional announcements as an example. A progress bar is not a live region, so even a correct one is not announced when its value changes, unless the screen reader decides to follow it. NVDA, for example, is set by default to beep as a progress bar changes, higher as it fills, while other screen readers may stay silent until the person moves to the bar. A beep doesn't say that the upload finished, or that it failed.
A bar is drawn to be watched, so anyone who isn't watching it misses how far it has got and the moment it ends. That includes sighted people who have moved on to the next field while the file uploads.
2 of 10 groups affected
- No vision (affected)
- Low vision (affected)
- Colour vision (not affected)
- No hearing (not affected)
- Hard of hearing (not affected)
- No speech (not affected)
- Motor (not affected)
- Reach, strength (not affected)
- Cognitive (not affected)
- Seizures (not affected)
- Without vision Screen reader users find no bar, or one that says 0% from start to finish, and hear nothing when the upload ends, so they upload the file again or send the form before it is attached.
- Limited vision Screen magnifier users zoomed in on the next field don't see the bar fill or the tick appear, and a spoken message at the end tells them without panning back.
axe reports a role="progressbar" or role="meter" with no name, but a bar made of div elements has no role for it to check, and it doesn't check native progress and meter elements for a name. No scanner can tell that aria-valuenow stays at 0 while the fill grows, or that the end is never announced, because it sees the page at one moment. Asked to fix the bar, code assistants may add role="progressbar" and the three aria-value* attributes with fixed numbers, which turns silence into a wrong answer, or wrap the bar and its percentage in aria-live, so that every percent is read out. Only a screen reader, used during an upload, shows what is heard.
Try it
Press Enter on Upload and listen while the bar fills.
The panel shows what a screen reader announces during the upload, and what it reads if you move to the bar. Do you hear how far along it is, and when it ends?
Broken Can you fix this?
Uploading report.pdf
Uploaded
Screen reader says:
(Press Upload)
The fix
Give the bar a name and a value that moves with it, and announce its milestones and its end.
- 1Say what each bar measures, in its name or in the text beside it.
- 2Set the value from the code that draws the fill.
- 3Announce milestones and the end in a status region, not every percent.
A bar drawn with divs
<p>Uploading report.pdf</p><div class="bar"><div class="fill" style="width: 0%"></div></div><p class="done" hidden>Uploaded</p>A native progress element
<label for="upload-bar">Uploading report.pdf</label><progress id="upload-bar" max="100" value="0">0%</progress><p class="done" hidden>Uploaded</p><p id="upload-status" role="status" class="visually-hidden"></p>Why this fix: Use native HTML first
The bar has no role, name or value, so a screen reader finds nothing after the file name, and "Uploaded" appears without being announced. Here upload() stands for the site's own upload code, calling back with a percentage as the file goes up, as the upload progress events of XMLHttpRequest allow.
To the browser, progress is a progress bar: the label names it, and its value against max is passed to assistive technology, so a screen reader that moves to it reads its name, its role and its value. The text inside it is a fallback for browsers that can't draw it, and does not name it. The status region is on the page from the start and empty, so its messages are announced politely, a quarter at a time, then "report.pdf uploaded." The cost is the look: browsers draw the bar themselves, and changing its colours takes their own pseudo-elements, such as ::-webkit-progress-value and ::-moz-progress-bar.
All three give the bar's progress in words that stay current, and announce milestones and the end from the same status region. The native progress element takes the least code: the browser supplies the role, the name from the label and the value, and the script sets only value; choose it when the bar can be restyled through its pseudo-elements. The custom role keeps a bar drawn any way the design wants, at the cost of an attribute the script must keep in step with the fill; choose it when the bar comes from a component you can't swap, or the design needs more than a track and a fill. The value as text is the smallest change to an existing bar and puts the number on screen for everyone; choose it when you can change the text around the bar but not the bar itself, and accept that screen readers then meet no progress bar, and NVDA no longer beeps as it fills.
Announce milestones, not every percent. An upload sends dozens of progress events, and a status message for each one becomes a queue of numbers the person has to sit through. Quarters suit a process that takes seconds. For a long one, such as a large export, say something every 10 seconds or so, or at each named stage ("Preparing", "Uploading", "Processing"). If it finishes within two or three seconds, announce only the end. Never put aria-live on the bar, or around a percentage that changes with it: that reads every change. A live text beside the bar, as technique ARIA25 shows, works when the script updates it only at milestones.
Steps, not percentages. A checkout or sign-up that shows "Step 2 of 4" as a bar has a value that isn't a percentage. Screen readers often read aria-valuenow as a percentage, so a value of 2 between 1 and 4 can come out as "2 percent". Set aria-valuetext="Step 2 of 4: Delivery" on the role="progressbar", or show the steps as a list with aria-current="step" on the current one, which says more than a bar can.
When there is no amount. If you can't know how far along it is, leave the value out: a progress element with no value attribute, or a role="progressbar" with no aria-valuenow, is indeterminate, a sign that something is happening. That is a loading state, with its own pattern in loading spinners that screen readers never announce.
Meters. A meter shows a measurement within a known range, such as storage used or a password's strength, not the progress of a task, and HTML says not to use meter for a progress bar. Give it a label, as for progress: <label for="storage">Storage used</label> before <meter id="storage" min="0" max="10" value="7.2">. Screen readers report meters unevenly: in tests published in 2022, NVDA called a meter a progress bar, VoiceOver on macOS skipped one that had been styled, and none of them read its low or high marks. So write the value as text beside the meter as well ("7.2 GB of 10 GB"), and say in words what its colour means ("Strength: weak"). A custom meter takes role="meter", a name, aria-valuenow, aria-valuemin and aria-valuemax (they default to 0 and 100), and an aria-valuetext with the units.
Colours and forced colors. Where the fill is the only place the value shows, it is a graphic that carries meaning, so it needs a contrast of 3:1 against the track; see low-contrast form fields, icons and focus rings. A fill drawn with background-color disappears in a forced colors theme, where backgrounds are replaced; give it background-color: CanvasText inside @media (forced-colors: active).
Fixing this with an AI coding assistant? Get this guide as Markdown, or get every guide in your coding agent.
Design ideas
A bar can be soft and still be read.
Eight ways to draw how far something has got, two of them moving. The guide’s fixes make each one readable to a screen reader; these are about whether everyone else can see it.
Passes
Uploading report.pdf
Fill at 3:1 or more against its track #1f5fa8 on a #e6e9ee track is 5.29:1. When the fill is the only place the value shows, it is a graphic that carries meaning, so it needs 3:1 under 1.4.11.Uploading report.pdf
Soft bar, with the percentage written beside it The pale fill is only 1.47:1 against its track, but “60%” says the value in text, so the fill is no longer needed to understand it. The bar can stay as soft as the design wants.Uploading report.pdf
Ring with the number in the middle The same value as an arc, for a compact card or a phone. The number carries the value, and the arc keeps 5.29:1 against its track anyway.Delivery detailsStep 2 of 4
Segments with the step written out For a checkout or a set-up flow, where progress is steps rather than percent. “Step 2 of 4” says where you are; the segments are a summary of it.Uploading report.pdf
Fill that glides to each new value Press Replay. The fill eases over 0.4 seconds each time the value changes, and stops when the value does, so it moves only to show progress. Under a reduced-motion setting it jumps instead.Rule of thumb: Move to show a change, then stop
Fails
Uploading report.pdf
Thin pale bar, nothing elseWCAG 1.4.11 The fashionable 4px bar: #a8c4e6 on #e6e9ee, 1.47:1, and no text. The fill is the only place the value shows, and many people can’t tell where it ends.report.pdf
Turns red when the upload failsWCAG 1.4.1 Colour alone says the upload failed: the bar looks the same as a running one to people who don’t see red. Write “Upload failed” and what to do next beside it.report.pdf uploaded
Stripes that keep moving after it finishesWCAG 2.2.2 The upload is done, but the barber-pole stripes go on sliding beside the rest of the form, with nothing to pause them. This sample stops after a few seconds; the pattern itself never does.Rule of thumb: Move to show a change, then stop
Verify the fix
4 checks, no mouse.
Test with: Screen reader, Keyboard
Record the OS, browser and assistive-technology versions, the build, the date, and the actual result of each step. Repeat after the upload code or the progress component changes.
Limits
Screen readers differ in how they report progress bars: some read the value as a percentage, some beep, and some speak only when the person moves to the bar, and their support for meters is weaker still. This guide covers one bar for one process on one page. A spinner or a wait with no amount is covered in loading spinners that screen readers never announce, and a message that isn't about progress, such as "Draft saved", in status messages that are shown but never announced. Test real uploads, slow and failing ones included, with the screen readers your users use.
This is a recreated teaching example, not a finding about a named client. The code is a starting pattern: test it in your own product. No completed assistive-technology test is claimed here.
References
- WCAG Understanding: name-role-value (opens in a new tab)
- WCAG Understanding: status-messages (opens in a new tab)
- WCAG Technique ARIA25 (opens in a new tab)
- developer.mozilla.org: progress (opens in a new tab)
- developer.mozilla.org: progressbar_role (opens in a new tab)
- developer.mozilla.org: meter_role (opens in a new tab)
- html.spec.whatwg.org: form-elements (opens in a new tab)
- htmhell.dev: 5 (opens in a new tab)
Keep this fixed as your code changes.
Turn guides like this into rules your coding agents and CI follow, then have the result retested with a keyboard and screen readers.