Why Do Screen Reader Users Miss Half of What Your App Does?
Why do screen reader users miss half of what your app does?
Because most product interfaces communicate through movement and colour, and neither is announced. A panel slides in, a row turns green, a toast appears and fades. To someone using a screen reader, nothing happened at all, because nothing moved the focus and nothing was placed in a live region.
This is the gap between a site that passes an automated scan and a product someone can actually operate. Automated tools catch missing labels and low contrast. They cannot tell you that your save confirmation is silent.
Two mechanisms close most of it: managing focus when the interface changes, and announcing changes that do not move focus. This piece covers both, with the standards they map to.
How bad is the baseline, really?
Worse than it was. The WebAIM Million analysed 1,000,000 home pages for its 2026 report and found detected WCAG 2 failures on 95.9% of them. The average page had 56.1 errors, a 10.1% increase on the previous year's 51.
The failures are concentrated. Low contrast text appeared on 83.9% of pages, missing alternative text on 53.1%, missing form input labels on 51%, empty links on 46.3%, empty buttons on 30.6% and missing document language on 13.5%. WebAIM notes that 96% of all detected errors fall into those six categories.
Fix those six and you have done most of what a scanner can see. The two topics in this article are what remains afterwards, and they are the ones that decide whether the product is usable rather than merely compliant.
What does focus management actually mean?
It means deciding, in code, where keyboard focus goes whenever the interface changes in a way a mouse user would follow with their eyes. Opening a dialog, revealing a panel, deleting a row, moving to a new step. Each of those is a moment someone has to be taken to.
WCAG 2.2, the current W3C Recommendation dated 12 December 2024, covers this at Success Criterion 2.4.3 Focus Order, Level A: if a page can be navigated sequentially and the sequence affects meaning or operation, components receive focus in an order that preserves meaning and operability.
The common failure is doing nothing. A dialog opens, focus stays on the button behind it, and the keyboard user tabs through the hidden page underneath while the dialog sits there unreachable.
Where should focus go when a dialog opens?
Into the dialog, on the first meaningful element, which is usually the heading or the first input rather than the close button. Then trap focus inside while it is open, and return focus to the element that opened it when it closes.
Returning focus is the half people forget. Without it, closing a dialog dumps the user back at the top of the document, and they have to navigate all the way to where they were. Do that three times and the product becomes exhausting.
Store the triggering element before you open, and focus it again on close. That is four lines of code and it is the difference between a usable dialog and a hostile one.
What does WCAG 2.2 add that is easy to miss?
Two focus criteria that are new in 2.2 and that modern product interfaces break constantly. Success Criterion 2.4.11 Focus Not Obscured (Minimum), Level AA, requires that when a component receives keyboard focus, it is not entirely hidden by author created content.
That one is aimed squarely at sticky headers, cookie banners and chat widgets. A focused element scrolled underneath a sticky header is a failure even though focus is technically correct, and it is extremely common in dashboards with a fixed top bar.
The second is 2.4.13 Focus Appearance at Level AAA, which sets a measurable bar for the indicator itself: an area at least as large as a 2 CSS pixel thick perimeter of the component, with a contrast ratio of at least 3:1 between the focused and unfocused states. Even if you are not chasing AAA, it is a useful specification to design to, and it beats the perennial instinct to remove the outline entirely. That instinct is already ruled out by 2.4.7 Focus Visible at Level AA.
What is a live region, and when do you need one?
A live region is an element whose content changes are announced by assistive technology without focus moving there. You need one whenever something important changes and you are not moving the user to it: a save confirmation, a validation summary, a search result count, an upload finishing.
WCAG covers this at Success Criterion 4.1.3 Status Messages, Level AA: status messages must be programmatically determinable through role or live region properties, without requiring the user to have focus on the message.
The word without is the whole criterion. A message the user has to find is not a status message. It is a hidden note.
Polite or assertive, and what is the difference?
Use polite for almost everything. MDN describes aria-live polite as meaning the screen reader will speak changes whenever the user is idle, which is right for confirmations, counts and progress updates.
Assertive interrupts whatever the screen reader is currently saying. MDN is blunt that this can be extremely annoying and disruptive, and it should be reserved for genuinely urgent messages, such as an error that blocks the user or a session about to expire.
The value off is the confusing one. It does not disable announcements. It means changes are announced only when focus is on or inside the element, which is also the implicit behaviour of roles like timer and marquee.
Should you use roles instead of aria-live?
Often, yes, because several roles carry live region behaviour implicitly and are easier to get right. The status role implies polite, the alert role implies assertive, and the log role implies polite and is designed for appended content such as a chat or an activity feed.
Our default in product UI is the status role for confirmations and counts, and the alert role for blocking errors only. That covers nearly everything without anyone having to reason about priority values.
Two related attributes matter when you do write them by hand. aria-atomic set to true announces the whole region rather than just the changed part, which is what you want for a value like a running total. aria-relevant controls which kinds of change are announced, defaulting to additions and text.
What is the mistake that makes live regions silently fail?
Creating the region and its content in the same moment. MDN's guidance is explicit: establish the live region before updating its content, starting with an empty live region and allowing time for it to be exposed to assistive technologies before the content changes.
In practice that means putting empty live regions in your initial markup, which MDN calls the most reliable approach, and writing text into them later. A toast component that mounts a div with the message already inside it will very often announce nothing at all.
If you genuinely must create the region dynamically, MDN suggests deferring the content update to a later task in the event loop. But the simpler fix is one persistent empty region per app, written to whenever you need an announcement.
How do you test any of this?
Keyboard first, screen reader second. Unplug the mouse and complete your product's three most important tasks. Every place you get stuck, lose your position, or cannot tell what happened is a defect, and you will find them in about twenty minutes.
Then turn on a screen reader and do the same tasks, listening specifically for the moments you know something changed. Silence is the bug. This is a different exercise from running a scanner, and it is covered further in how to actually test with a screen reader.
Automated tools still earn their place for the six categories above. They just cannot hear silence, which is why both passes are needed.
Where should a product team start?
Three things, in order. Add one empty polite live region to your app shell and route every confirmation and error message through it. Fix focus on your dialog component, including returning focus on close. Then check that no focused element ends up underneath your sticky header.
Those three cover the majority of what keyboard and screen reader users hit in a typical dashboard, and they are component level fixes rather than a programme of work. One dialog component usually serves the whole product.
It also makes your error and status design better for everyone, because a message good enough to announce is a message written clearly, which is the same argument we made in designing error messages people can act on.
We build product interfaces alongside the marketing sites that sell them, and this is the layer that most often gets deferred and then never scheduled. If you want a review of where your product goes quiet, we are happy to do that with you. You can find us at phoenix.studio, and the broader standard is covered in our WCAG guide.
Want a site that performs like this?
Tell us about your project. We will come back with a clear next step, no pressure.
This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.
Have a project like this?
Tell us where you want to go. We'll tell you how we'd get you there.