Most teams still treat screen reader support as a checkbox. Add some ARIA, paste in an “aria-label”, pass an automated audit, call it a day. Then a blind user tries to navigate the site and gets dumped into a wall of unlabeled buttons, focus traps, and random reading order. I learned the hard way that passing automated tests does not mean your UI is actually usable with a screen reader.
The short answer: designing for screen readers means treating your DOM as the real UI, not the pixels. That means proper semantic HTML, a logical heading and landmark structure, predictable focus management, real keyboard support, and only then careful ARIA. If a sighted tester can “see” how to use your interface but a screen reader user cannot “hear” the same structure and feedback, your accessibility layer is broken, no matter how pretty your React components look.
What screen readers actually do (and what they need from you)
Screen readers do not look at your layout or your CSS. They talk to the accessibility tree exposed by the browser, which is built from:
- Native HTML elements and attributes
- ARIA roles, states, and properties
- The current focus position and selection
- Live region updates
They then present that in different modes:
- “Browse” or “virtual cursor” mode: arrow through content, jump by headings, regions, links, form fields.
- “Focus” or “forms” mode: interact directly with widgets, use space / enter / arrow keys to trigger behavior.
- Shortcut navigation: H for next heading, D for next landmark, F for next form field (varies per screen reader).
If your DOM order, roles, and labels do not match what a sighted user sees, a screen reader user is effectively on a different website.
So the design target is not just “works with VoiceOver” or “passes Lighthouse”. The target is: the accessibility tree presents a clean, logical, predictable model of your UI.
Semantic HTML first, ARIA later
The single biggest boost for screen reader support is to stop fighting the platform and use native HTML elements correctly.
Use real elements instead of fake widgets
Here is a simple truth: a plain `
| Need | Preferred HTML | Common anti-pattern |
|---|---|---|
| Clickable action | <button> | <div role=”button” tabindex=”0″> |
| Navigation | <a href=”…”> | <span onclick=”…”> |
| Form label | <label for=”id”> | Placeholder text only |
| Section heading | <h1>..<h6> | <div class=”big-text”> |
| List of items | <ul>/<ol> with <li> | Sequence of <div> wrappers |
Browsers already know how to expose these as accessible objects with role, name, and state. Screen readers already know how to announce and navigate them.
If you need ARIA role=”button” on a div, first ask why you are not using a button.
There are valid reasons for custom widgets, but they are rare compared to how often teams reach for ARIA out of habit.
Keep DOM order in sync with visual order
Screen readers read according to DOM order, not what you achieved with CSS grid tricks.
If you visually put the “Continue” button on the right but keep it near the top of the DOM, a blind user will encounter it too early. They might activate it before they have actually heard the validation errors or form explanation that comes after.
Some rules:
- Order elements in DOM according to the reading flow you expect.
- Do not rely on “order” in flexbox or grid to reorder critical content.
- For mobile-first, watch out for layouts that rearrange content heavily at larger breakpoints; keep DOM flow stable.
Headings: your navigation spine
Many experienced screen reader users jump by headings almost constantly. They will never read your page linearly.
You should:
- Have exactly one logical top-level heading for the main content area (often an h1, or h2 if you embed inside a larger template).
- Use heading levels to reflect structure, not styling. Heading size is CSS; hierarchy is h2, h3, h4, etc.
- Avoid skipping from h2 to h5 with no h3/h4 just because the CSS looked better.
A clean heading structure:
“`html
Account settings
Profile information
Security
Two factor authentication
Active sessions
Notifications
“`
This lets someone hit “H” or the “next heading” key to scan the page in a structured way.
Landmarks and regions: give users a map
Landmarks are higher level navigation targets. They answer “where am I in the layout?” Screen readers expose them via keyboard shortcuts.
The key ones:
<header>for page or section header<nav>for primary and secondary navigation<main>for the main content area (one per page)<aside>for complementary content, sidebars<footer>for footer content- ARIA landmarks like
role="search",role="banner", etc., where HTML is not enough
Example layout skeleton:
“`html
Acme Control Panel
Billing overview
…
“`
The ‘Skip to main content’ link is not optional padding. It is central to keyboard and screen reader comfort.
Add a skip link that appears on focus:
“`html
Skip to main content
“`
Style it so it moves off-screen visually but becomes visible when focused.
Labels, names, and instructions
Screen readers depend heavily on accessible names. If you have a sleek minimalist UI full of icons without text, screen readers will output “button, button, button”. That is not a UI.
Label every control clearly
Use `
“`html
“`
If the visual design does not allow text, keep the label off-screen rather than remove it:
“`html
“`
The “sr-only” class typically positions text for screen readers, not for sighted users.
For icons in buttons:
“`html
“`
Or include visible text and avoid the aria-label entirely:
“`html
“`
For links that wrap complex content (card tiles, etc.), text still needs to carry meaning:
Pro plan
For growing communities that need more control.
“`
Do not rely on “Read more” or “Learn more” repeated twenty times on a page without context. Screen readers often provide a list of links; “Read more” repeated is useless. Use unique link text:
“`html
Read more about screen reader testing
“`
Describe context, not just single fields
If a field needs instructions, connect them via `aria-describedby`:
“`html
Do not include http:// or https://
“`
Screen readers will read the label and then the description.
Keyboard and focus: the control path users actually follow
Screen reader users rely on the keyboard to move focus and activate controls. If your app is not keyboard-friendly, it is not screen reader-friendly, no matter how perfect your ARIA is.
Basic keyboard rules
- Every interactive element must be reachable with Tab (or Shift+Tab backwards).
- Use native elements. `
- Never remove outline styles without providing a visible focus replacement.
- The focus order must follow DOM order and match visual order.
Custom controls like tab lists, menus, sliders, and tree views need additional keyboard patterns:
| Widget | Expected key behavior |
|---|---|
| Tablist | Arrow keys to move between tabs, Tab into panel content. |
| Menu | Arrow keys move through items, Enter/Space to activate, Esc to close. |
| Dialog | Focus trapped inside, Esc to close, initial focus on meaningful element. |
| Slider | Arrow keys change value, Home/End jump to min/max. |
Do not improvise custom patterns. Follow the ARIA Authoring Practices where possible.
Focus management around dialogs and overlays
Modals are a frequent source of frustration for screen reader users.
Correct behavior:
- When the dialog opens, move focus to the first meaningful focusable element, usually the dialog heading or a primary control.
- Trap focus inside the dialog while it is open. Tab should cycle within dialog controls only.
- On close, return focus to the element that opened the dialog.
- Make background content inert while the dialog is open, either by removing it from the accessibility tree or using patterns like aria-hidden on rest of the page.
Markup example:
“`html
Settings
“`
Screen readers rely on `role=”dialog”` and `aria-modal=”true”` to treat it as a separate interaction context.
A modal that is visible but not announced is worse than no modal at all; it blocks the UI and leaves the user stranded.
Using ARIA without shooting yourself in the foot
ARIA is powerful and also easy to misuse. Wrong ARIA can make a UI less accessible than having no ARIA.
Simple ARIA rules that prevent most damage
- Prefer native HTML semantics over ARIA whenever possible.
- Do not change the native role of elements. Avoid role=”button” on a `
- Avoid ARIA where you do not fully understand the impact.
- Do not hide visible content from screen readers unless it is truly decorative or redundant.
Common useful ARIA patterns:
aria-labelfor elements that have no visible label (as a last choice).aria-labelledbyto reference an existing visible label or heading.aria-describedbyfor extra context.role="alert"oraria-livefor notifications.aria-expandedandaria-controlsfor expandable UI segments.
Example of a disclose control:
“`html
“`
When the user activates the button, toggle `aria-expanded` and the hidden attribute. Screen readers will announce “expanded” or “collapsed”.
Live regions for async changes
Modern web apps update content without page reloads. Sighted users see these changes. Screen readers may not.
Use live regions sparingly to announce critical updates:
“`html
“`
Then, when some async event finishes:
“`js
document.getElementById(‘notification-region’).textContent =
‘Your DNS records have been updated.’;
“`
Use `aria-live=”polite”` for non-urgent copy and `role=”alert”` or `aria-live=”assertive”` for errors that demand attention. Overuse of assertive regions will make the interface noisy and frustrating.
Structuring complex web apps for screen readers
If you are building a control panel, admin interface, or management console for hosting or communities, your UI will not be static. It is a single-page app with nested routes, panels, and many custom components. That setup creates special demands.
Handling virtual page changes in SPAs
Screen readers track navigation events and focus. If you use client-side routing, the URL and the visible view change, but there is no new page load for the screen reader to detect.
Minimum behavior on route change:
- Move focus to a logical heading or the main content container.
- Update the document title with the new view name.
- Announce the change in a polite live region if needed.
Example pattern in a React-like router:
“`js
useEffect(() => {
document.title = `DNS Records – Control Panel`;
const heading = document.querySelector(‘h2#page-title’);
if (heading) {
heading.setAttribute(‘tabindex’, ‘-1’);
heading.focus();
}
}, [routePath]);
“`
Then:
“`html
DNS records
…
“`
This gives screen reader users a clear signal that the “page” changed even though the browser stayed on the same document.
Grids, tables, and data-heavy UIs
Admin UIs and hosting dashboards tend to have dense tables: logs, billing rows, resource lists. Screen readers can handle tables well if they are coded correctly.
Core rules:
- Use `

