Search topics
Loading spinners that screen readers never announce
A spinner and a "Loading…" line appear on screen, but nothing tells a screen reader that the page is busy or that the results have arrived.
The problem
Screen reader users press a button and hear nothing, so they can't tell whether it worked, is still working, or failed.
On an account page, a "Show orders" button fetches past orders. A spinner turns and the line "Loading orders…" appears under the button. A second later the spinner goes and three orders appear. A sighted user follows every step. A screen reader user presses the button and hears nothing: not the start, not the end. Focus is still on the button, so they press it again, or move on and miss the orders.
"Loading orders…" and "3 orders loaded" are status messages: they report the progress and result of an action without moving focus. Success Criterion 4.1.3 asks that such messages can be presented to assistive technology without receiving focus. Here the text is added to a plain div, so the change is visible but never announced. The spinner is a styled span with no text at all.
The spinner is drawn for people who are looking at it, so anyone who isn't gets no sign that anything happened.
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 can't tell whether a request started, is still running, finished or failed. They repeat the action, which can send it twice, or leave before the content arrives.
- Limited vision Screen magnifier users zoomed in on the button can miss a spinner drawn elsewhere on the page, and a spoken update helps them too.
A scan of the loaded page sees a list of orders and passes it; it never sees the busy state in between. Many scanners can't tell a status message from any other text. AI code often adds aria-live to the element that the script creates with its text already inside, and many screen readers ignore a region that arrives with its content. Others add aria-busy and expect it to say "loading", which it doesn't. Only a screen reader, used during the load, shows whether anything is said.
Try it
Press Enter on Show orders and listen.
The panel shows what a screen reader announces while the orders load. Is the wait ever put into words?
Broken Can you fix this?
Screen reader says: (Press Show orders)
A recreated demo. The broken version is intentionally inaccessible.
The fix
Write the loading and finished messages into a live region that was there before the request.
- 1Render the live region empty on page load, then change its text.
- 2Announce the start, the result and any error, in words.
- 3Keep focus where it is; don't announce by moving it.
<button type="button" id="show-orders">Show orders</button><div id="orders-state"></div><ul id="orders"></ul>The text appears and disappears on screen, but nothing tells assistive technology it changed, and the result is never put into words at all.
The p with role="status" is in the HTML from the start and empty, so the browser is already watching it when the text changes. role="status" is a polite live region: the screen reader finishes what it is saying, then reads "Loading orders…", and later "3 orders loaded." Focus stays on the button. Draw the spinner with CSS on the region or mark it aria-hidden="true"; the words carry the meaning.
Both regions announce the loading text and the result without moving focus. Use the status region: loading and loaded are routine news, and role="status" delivers them without cutting anyone off, for the same code. The alert region is right only when the news can't wait. If both kinds of message come from the same action, use one region of each: routine progress in the status region, the failure in the alert region.
The region must exist first. Render it empty with the page and change only its text. A region added together with its message is often not announced. In a component framework, keep the region outside any block that mounts with the loading state.
The role is the live region. role="status" already means aria-live="polite" and aria-atomic="true", and role="alert" means aria-live="assertive", so neither needs an aria-live attribute beside it. Some teams add a matching aria-live as a fallback for very old screen readers; it does no harm, but never pair a role with the other politeness, as in role="status" aria-live="assertive". A plain <div aria-live="polite" aria-atomic="true"> with no role works too; without aria-atomic, some screen readers read only the part of the message that changed.
Say it once. Don't write a percentage into the region on every tick; people hear a stream of numbers. Announce the start, maybe a slow-load update after a few seconds ("Still loading…"), then the end. For a visible progress bar, use <progress> beside the region, not in it.
Short loads. If the result arrives within a fraction of a second, you can skip "Loading…" and announce only the result. If the same text is written twice in a row, some screen readers stay silent; clear the region first, or vary the message ("3 orders loaded" then "Orders refreshed").
Not fixes on their own. aria-busy="true" tells assistive technology to wait before reading a region; it does not say "loading". Moving focus to the new content works for a page-like change, but it can't announce the busy state and it takes people away from where they were. The <output> element maps to the status role, but screen readers announce its changes less reliably than an explicit role="status".
Errors. A failed request needs words too. Put it in the same status region, or in an alert region if the person must act now.
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 data layer or loading component changes.
Limits
Live regions behave differently across screen readers and browsers, especially for regions that are hidden, moved, or inside dialogs. This pattern covers one loading action on one page. Test your real flows, including route changes and infinite scroll, 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.
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.