Back to Accessibility
Accessibility
medium
mid

How do you ensure a web application is accessible to screen reader users?

Build on semantic HTML so the screen reader gets role/name/state for free, ensure full keyboard operability, give every control an accessible name, manage focus on dynamic changes, announce async updates with aria-live, and verify by actually navigating with VoiceOver/NVDA — not just running axe.

6 min read·~10 min to think through

Ensuring accessibility — especially for screen readers — comes down to making sure every element exposes a correct name, role, and state, and that the whole app is operable without a mouse.

1. Semantic HTML is the foundation

A screen reader builds its model from the accessibility tree, which the browser derives from your HTML. Native elements populate it for free:

html
<button>Delete</button>
<!-- announced: "Delete, button" — focusable, Enter/Space work -->

<div class="button" onclick="...">Delete</div>
<!-- announced: "Delete" — not focusable, no role, invisible to SR users -->

Use real <button>, <a href>, <input>, <nav>, <main>, headings in order, lists for lists, <table> for tabular data. This alone solves most problems.

2. Accessible names for everything

Every interactive element must have a name:

html
<label for="email">Email</label><input id="email" type="email" />
<button aria-label="Close dialog">✕</button>
<img src="chart.png" alt="Revenue grew 20% in Q3" />
<img src="divider.png" alt="" />  <!-- decorative: empty alt -->

3. Full keyboard operability

If you can't reach and use it with Tab / Shift+Tab / Enter / Space / Arrows / Escape, a screen reader user can't either.

  • Logical tab order (follow DOM order, avoid positive tabindex).
  • Visible focus indicator — never outline: none without a replacement.
  • No keyboard traps; Escape closes overlays.
  • Custom widgets follow ARIA Authoring Practices key patterns.

4. ARIA to fill the gaps

When no native element fits, ARIA describes role/state — but you must add the behavior:

html
<div role="tablist">
  <button role="tab" aria-selected="true" aria-controls="panel-1">Profile</button>
</div>
<div role="tabpanel" id="panel-1">...</div>

5. Announce dynamic changes with live regions

SPAs change content without a page reload — screen readers won't notice unless you tell them:

html
<div aria-live="polite">3 results found</div>
<div role="alert">Payment failed — try again</div>  <!-- assertive -->

polite waits for a pause; assertive/role="alert" interrupts. Use assertive only for errors.

6. Manage focus

  • On route change in an SPA, move focus to the new page's <h1> (or a skip target).
  • When a modal opens, focus moves into it and is trapped; on close, focus returns to the trigger.
  • Never let focus land on hidden elements.

7. Test with a real screen reader

Automated tools (axe, Lighthouse) catch only ~30–40%. The rest needs:

  • VoiceOver (Cmd+F5 on Mac), NVDA (free, Windows), TalkBack (Android).
  • Navigate by headings, landmarks, and form controls — the way real users do.
  • Tab through every flow with no mouse.

Senior framing

The strong answer frames screen-reader support as a byproduct of correct semantics and keyboard support, not a separate "ARIA layer" bolted on. Call out the accessibility-tree mental model, the name/role/state triad, focus management in SPAs, and that you've actually used VoiceOver/NVDA — that last point separates people who've done it from people who've read about it.

Follow-up questions

  • What is the accessibility tree and how is it built?
  • When do you use aria-live='polite' vs 'assertive'?
  • How do you handle focus on route changes in a SPA?
  • What's the difference between aria-label, aria-labelledby, and aria-describedby?

Common mistakes

  • Using div/span with click handlers instead of button/a.
  • Adding ARIA roles but no keyboard behavior.
  • Forgetting that SPA route changes are silent to screen readers.
  • Testing only with axe and never with an actual screen reader.
  • Putting aria-live on an element that's added to the DOM at the same time as the message (it must already exist).

Edge cases

  • aria-live regions must be present in the DOM before the content changes, or the change isn't announced.
  • Visually hidden text (.sr-only) must not use display:none — that removes it from the accessibility tree.
  • Toasts that auto-dismiss may vanish before a screen reader user reaches them.

Real-world examples

  • Skip-to-content links, accessible form validation, and live cart-count updates in e-commerce.

Related questions