Accessibility (A11y): Designing for Screen Readers

Accessibility (A11y): Designing for Screen Readers

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 `
“`

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:

“`html

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

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

“`

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

Common useful ARIA patterns:

  • aria-label for elements that have no visible label (as a last choice).
  • aria-labelledby to reference an existing visible label or heading.
  • aria-describedby for extra context.
  • role="alert" or aria-live for notifications.
  • aria-expanded and aria-controls for 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 `
    `, `

    `, `

    `, `

    ` for headers.
  • Use `scope=”col”` on column headers and `scope=”row”` for row headers.
  • Keep tables simple when possible; complex nested tables are difficult.
  • Example:

    “`html

    Domain Status Expires Actions
    example.com Active 2026-01-01

    “`

    If you build custom “tables” from divs, you must recreate table-like semantics with ARIA roles. That is error-prone. Start with a real table.

    Error states, validation, and feedback

    If something goes wrong, the user must hear it in a clear, structured form.

    Field-level errors

    The pattern:

    • Associate errors with the relevant field using `aria-describedby`.
    • Use clear text, not just “invalid”. Explain the required format or rule.
    • Use role=”alert” or a live region for form-level errors that appear after form submission.

    Example:

    “`html

    “`

    Form submission feedback

    Global error summary:

    “`html

    “`

    Move focus to the error summary heading on failed submit. Each link jumps to the problematic field.

    Errors that live only in red borders and tiny inline text are invisible to screen reader users.

    Testing with real screen readers, not just automation

    Automated testing tools catch basic issues. They will not tell you if the UX is painful or confusing.

    You need at least periodic manual testing with:

    • NVDA on Windows (free, widely used).
    • JAWS on Windows (paid, heavy presence in enterprise and government).
    • VoiceOver on macOS and iOS (built in).

    Basic manual flow to test:

    • Disable your mouse.
    • Turn on the screen reader.
    • Navigate your app from login to a real task (for example: purchase a plan, change DNS record, manage community members).
    • Listen carefully to what is announced, the order, and when you feel lost.

    Questions to ask yourself:

    • Can I get to the main content quickly using a skip link or landmarks?
    • Does every page start with a clear heading that states where I am?
    • Do buttons and links make sense out of context?
    • When a modal opens, does the screen reader announce it and move focus inside?
    • When the route changes, do I hear a new title or heading?
    • Are error messages spoken without me hunting for them?

    If any of those fail, it does not matter what your automation score says. You have work to do.

    Common mistakes and how to fix them

    1. Overuse of aria-hidden

    Hiding the wrong things from screen readers breaks navigation.

    Bad pattern:

    “`html

    “`

    This hides the heading from assistive tech and removes a key navigation target.

    Use aria-hidden on:

    • Pure decorative icons.
    • Visual-only duplicate information that has another accessible source.

    Do not use it to silence text you think is “unimportant”. If it is visible and helps orientation, screen reader users need it too.

    2. Icon-only buttons with no text alternative

    Developers love minimalist icon buttons. Screen readers hate “button, button, button”.

    Fix:

    • Add `aria-label` that describes the action, not the icon.
    • Prefer visible text next to the icon for clarity.

    3. Bad use of role=”presentation” or role=”none”

    These roles strip semantics and tell screen readers to ignore structure. That is correct for decorative tables or icon wrappers, but dangerous on content.

    Example of safe use:

    “`html

    “`

    Here the list structure is not important, but links are.

    Do not add role=”presentation” on headers or content-bearing tables.

    4. Fake disabled controls

    Developers often “disable” a button by reducing opacity and removing the click handler while leaving it focusable and with the same label. Screen readers will still land on it and the user will not know why it does nothing.

    Instead:

    • Use the `disabled` attribute on form controls where applicable.
    • Use `aria-disabled=”true”` if you cannot use the native disabled attribute.
    • Provide context: “Submit (disabled: fill required fields)” in label if needed.

    5. Infinite scroll without structure

    Admin UIs with endless log lists or user lists often load more items as the user scrolls. For screen readers, focus might jump unpredictably, and there is no sense of progress.

    Fix patterns:

    • Group items clearly with headings or region labels.
    • Announce when more items load in a live region (“Loaded 50 more log entries”).
    • Provide a “Load more” button as an alternative to endless auto-load.

    Designing with screen reader users in mind from day one

    Retrofitting existing apps is always expensive. The better path is to bake screen reader support directly into your design system and component library.

    Component library contracts

    Each component should specify:

    • Its role and keyboard interaction pattern.
    • Required ARIA attributes and label props.
    • How it handles focus on open/close.
    • How it exposes state updates to assistive tech.

    For example, a “Modal” component contract:

    • Requires a title prop, which maps to `aria-labelledby`.
    • Sets `role=”dialog”` and `aria-modal=”true”`.
    • Traps focus within while open.
    • Returns focus to trigger element on close.

    A “Tabs” component contract:

    • Each tab has a button with role=”tab”.
    • Panels have role=”tabpanel” and aria-labelledby referencing the tab.
    • Arrow keys move between tabs; Tab moves into content.

    Once these patterns are correct in the shared components, product teams have less room to break things.

    Design specs that protect screen reader UX

    Designers should:

    • Specify headings explicitly in comps (not just big text).
    • Call out where ARIA relationships are needed (for example: which element labels which).
    • Propose error summaries and their content, not only red borders.
    • Avoid layouts that rely on heavy visual reordering compared to DOM.

    If your design file does not show a keyboard path and a heading structure, it is not done.

    Accessibility debt and ongoing maintenance

    You will not fix everything in one pass. Treat screen reader support like other quality attributes: track it, test it, maintain it.

    Practical steps:

    • Include basic screen reader checks in review checklists.
    • Add automated axe or similar checks to CI to catch regressions.
    • Schedule periodic manual runs through core flows using at least one screen reader.
    • Collect feedback from real blind users periodically, not just internal engineers guessing.

    If you run a hosting or community platform, your target audience includes developers and power users who often rely on assistive tech. They will notice sloppy work here.

    In the end, designing for screen readers is about respecting the accessibility tree as the primary interface. Treat semantics, focus, and announcements with the same attention as colors and typography, and your app will be far more usable to the people who rely on it every day.

    Diego Fernandez

    A cybersecurity analyst. He focuses on keeping online communities safe, covering topics like moderation tools, data privacy, and encryption.

    Leave a Reply