You’ve shipped the polished interface. The visual review passed, the automated audit is green, and the component library looks consistent. Then someone runs the account form with NVDA and finds that the custom select announces nothing, the dialog leaves focus behind the overlay, and the validation message appears on screen without being read aloud. The UI looks finished, but its interaction model is incomplete.
Screen reader compatibility means more than adding labels or removing a few audit violations. It means making sure assistive technology can identify each control, understand its current state, follow the user’s focus, and receive meaningful updates as the interface changes. That behaviour has to hold across browsers, operating systems, and screen readers, not just in the development environment.
Table of Contents
- Why Screen Reader Compatibility Breaks in Modern UIs
- Core ARIA Patterns That Make Components Accessible
- Focus Management Techniques for Screen Reader Users
- Testing Compatibility Across NVDA VoiceOver and TalkBack
- Common Pitfalls and How to Fix Them
- Building a Repeatable Accessibility Workflow
Why Screen Reader Compatibility Breaks in Modern UIs
A native button usually gives a browser enough information to expose its role, name, and keyboard behaviour. A div styled to look like a button gives the browser almost nothing unless the team adds the role, focus handling, keyboard interaction, and state changes itself. That distinction explains why a page can pass a visual review while failing the first real screen reader task.
The failure often appears in a familiar sequence. A developer opens a custom dropdown with a mouse, sees the options, and assumes the interaction works. A screen reader user tabs to the trigger, hears an unhelpful label, presses Enter, and receives no indication that the list opened. If the implementation also moves focus incorrectly, the user may continue reading unrelated page content without knowing that a selection menu is available.
Dialogs create a similar problem. The overlay may be visually obvious, but focus can remain on the trigger, move to the document behind the dialog, or disappear entirely. Dynamic notifications are just as easy to miss. A toast can appear in the DOM and remain completely silent if it has no suitable live-region behaviour or if the update is made in a way the screen reader doesn’t announce.

The semantic HTML boundary
The safest starting point is still native HTML. Use button for actions, a for navigation, input and label for fields, and select where the native interaction meets the product requirement. Native controls carry established semantics and keyboard behaviour, reducing the amount of compatibility code the team has to maintain.
Custom widgets are justified when the native control cannot provide the required interaction, but they create a behaviour contract. The DOM must expose the right role, state, relationships, and focus model. The keyboard must behave as users expect, and the accessibility tree must update when the component opens, closes, selects, loads, or fails.
Practical rule: If a native element can express the interaction, start there. ARIA should enhance a necessary custom control, not disguise a broken one.
UK public-sector teams have a clear benchmark to work against. GOV.UK service guidance requires services to meet WCAG 2.2 level AA and work with commonly used assistive technologies, making compatibility an implementation responsibility rather than a final checklist item. The GOV.UK assistive technology survey found that 29% of assistive-technology users accessed GOV.UK with a screen reader, with JAWS, VoiceOver, and NVDA among the most common readers. Testing one product alone can’t represent the ecosystem.
Retrofitting often fails because the component’s visual assumptions are already embedded in event handlers, CSS, and state management. Fixing the semantics then exposes deeper problems, such as an option list that can’t receive focus, a modal that has no restoration target, or an error state that exists only as a colour change. Building the interaction contract into the component from the start is cheaper and more reliable than repairing each consuming page.
Core ARIA Patterns That Make Components Accessible
ARIA supplies the metadata that custom controls need to communicate with assistive technology. It doesn’t create keyboard behaviour, focus movement, validation, or usable content by itself. A component is compatible only when its role, state, properties, and interaction logic agree.

Use native semantics before ARIA
A native button already has a button role. Adding role="button" to it is redundant, and applying a conflicting role can make the accessibility tree less trustworthy. The same principle applies to links, headings, and form controls. Give the browser a semantic element first, then add ARIA only where the native element cannot represent the custom interaction.
For naming, visible text or an associated label is preferable to hidden naming attributes. An icon-only button may need an accessible name, but the name should describe the action, such as “Open menu”, rather than the implementation. The UX design principles from Figr are useful background for keeping the visible interaction and the underlying task aligned.
Dropdowns and listboxes
A custom selection control commonly has a trigger and a popup list. The trigger should expose whether the list is open with aria-expanded, and it should identify the controlled popup with aria-controls. The list can use role="listbox", while each choice uses role="option" and exposes its selected state with aria-selected.
The implementation also needs a keyboard model. Tab should enter the widget predictably, arrow keys should move through options, and Enter or Space should commit a choice. The selected value must be reflected in the trigger’s accessible name or associated text, otherwise users may hear the available options without knowing which value is active. For a production pattern, see this guide to an accessible dropdown menu.
Dialogues, tabs, and autocomplete
A modal window needs role="dialog" or role="alertdialog" when the content requires an urgent response. It should have an accessible name through aria-labelledby or an equivalent relationship, keep focus within the modal while open, and restore focus to the invoking control when it closes.
Tabs require a relationship between each tab and its tabpanel. The active tab should expose aria-selected, and the tablist should provide a coherent keyboard path. Autocomplete fields are more involved. A combobox needs to expose its expanded state, identify the popup, and communicate the active option, often through aria-activedescendant. Loading, empty, and error states also need a deliberate announcement strategy.
aria-live is appropriate for content that changes without moving focus, such as a validation summary or a status message. Use it carefully. If every keystroke creates a live update, the announcement stream becomes noisy and users lose control of the task. If the update changes the user’s context, moving focus may be clearer than announcing a distant status region.
Focus Management Techniques for Screen Reader Users
Focus is the user’s position in an interactive interface. Screen readers can browse content independently, but when a user operates a button, opens a menu, or submits a form, focus tells assistive technology where the next interaction belongs. A visually correct component with incorrect focus behaviour feels broken because the user has lost the interface’s current location.
A dialog demonstrates the required sequence. When the trigger opens it, focus should move to a meaningful control inside the dialog, usually the heading only when that supports the task, or the first actionable field. While the dialog remains open, Tab and Shift+Tab should cycle within it. Escape should close it when the design permits, and focus should return to the original trigger.

Fixing focus that vanishes
A common production bug comes from rendering a panel conditionally and assuming the browser will place focus inside it. It won’t. The panel can exist visually while focus remains on a removed trigger or lands on the document body. Store the invoking element, render the panel, wait until its target is available, and call focus() on that target. On close, verify that the stored trigger is still connected before restoring focus.
Drawers need the same discipline as modals. A drawer that opens from a toolbar should expose its name, prevent interaction with background content while active, and provide an obvious close control. Avoid sending focus to a decorative container. A focusable heading can help orient a user, but the first useful action should remain easy to reach.
Managing composite widgets
Menus, listboxes, and tablists shouldn’t put every internal item into the page’s Tab sequence. A roving tabindex pattern gives the composite widget one entry point, then uses arrow keys to move the active item. Another approach keeps focus on the container and uses aria-activedescendant to identify the active option. Pick one model and test it consistently across supported combinations.
Dynamic content requires a separate decision. A toast that confirms a background action can use a polite status region so it doesn’t interrupt the current task. A form error that prevents submission needs stronger treatment. Associate the message with the field using aria-describedby, expose the invalid state with aria-invalid, and give the user a clear route to the first failing control or summary.
When focus moves, the user should understand why. When focus doesn’t move, the updated information must still be discoverable.
Headless primitives can reduce the amount of this logic that each team writes independently. DOM Studio provides framework-agnostic component behaviour with Vue wrappers, including ARIA wiring and focus handling for controls such as dialogs, dropdowns, listboxes, and comboboxes. That doesn’t remove the need for task-level testing, but it can prevent every product team from rebuilding the same fragile focus model.
Testing Compatibility Across NVDA VoiceOver and TalkBack
Automated tools can identify missing labels, invalid attributes, and some structural problems. They can’t tell you whether a user hears the selected option after pressing an arrow key, whether a modal announces its title at the right moment, or whether a toast interrupts a critical task. Manual testing must follow the same task flows users follow, not a tour of isolated components.
UK testing guidance expects services to work with specific assistive-technology and browser combinations before public beta. The recommended coverage includes JAWS with Chrome or Edge, NVDA with Chrome, Firefox, or Edge, VoiceOver on iOS with Safari, and TalkBack with Chrome, alongside speech and magnification tools. The GOV.UK guidance on testing with assistive technologies makes the matrix explicit.
A practical test loop
Start with a clean page and a realistic task. Turn on the screen reader before interacting, then move through the page by landmark, heading, form control, and Tab order. Don’t rely only on the visual cursor. Record what the reader announces, where focus moves, and whether the next action is obvious without sight.
For each component, test the closed state, opening action, active state, selection or submission, error state, and closing action. Repeat the flow with keyboard input on desktop and touch exploration or gestures on mobile. A component that works with NVDA and Chrome may still expose different timing or naming behaviour in VoiceOver and Safari.
| Screen Reader | Browser | Platform | Priority |
|---|---|---|---|
| NVDA | Chrome | Windows | Core desktop coverage |
| NVDA | Firefox | Windows | Desktop compatibility comparison |
| VoiceOver | Safari | iOS | Mobile coverage |
| VoiceOver | Safari | macOS | Apple desktop coverage |
| TalkBack | Chrome | Android | Android mobile coverage |
| JAWS | Chrome or Edge | Windows | UK public-sector benchmark |
What to document
Write failures as reproducible task steps, not subjective descriptions. “Dialog is inaccessible” doesn’t tell an engineer what to fix. “Activate Add address, press Tab twice, focus moves behind the overlay, and Escape doesn’t close the dialog” does.
Capture the expected announcement and the observed announcement. Note the browser, operating system, screen reader version, input method, and whether the issue occurs after a fresh load or only after a state change. This detail matters because screen readers may expose the same DOM differently depending on timing and platform integration.
A useful screen reader testing workflow should include a short regression script for every shared component. Prioritise failures that block task completion, hide errors, strand focus, or make a control’s state unknowable. Perfect announcement parity isn’t always realistic, but users must be able to enter, understand, operate, and leave each interaction.
Common Pitfalls and How to Fix Them
The most serious failures aren’t always invalid ARIA. Sometimes the interaction itself is the problem. Glasgow City Council states that ranking and slider questions aren’t accessible with screen readers, recommends splitting matrix questions into individual multiple-choice questions, and requires alternative text for non-text content in compatible survey versions. That guidance points to a practical truth: teams sometimes need to replace an interaction model rather than decorate it with attributes.

Interaction patterns that need redesign
A slider may be technically focusable but still difficult to operate, understand, or review in a complex survey. A ranking drag-and-drop interaction can fail even when each item has a label. Use a native radio group, an ordered set of buttons, or a separate accessible version that preserves the same question and records equivalent answers.
The same reasoning applies to matrix questions. Splitting a dense grid into single questions usually creates a longer visual form, but it gives each response a clear label and a simpler reading order. This is a trade-off worth making when the original control prevents a user from completing the task.
Before and after patterns
A visually styled clickable container is a common code-review failure:
<div class="select-trigger">Choose a department</div>
It has no native role, no keyboard activation, and no state. If a custom control is necessary, use a real button for the trigger and expose the popup state:
<button
type="button"
aria-expanded="false"
aria-controls="department-options">
Choose a department
</button>
<ul id="department-options" role="listbox" hidden>
<li role="option" aria-selected="false">Finance</li>
<li role="option" aria-selected="false">Support</li>
</ul>
That markup is only the starting point. JavaScript still has to update aria-expanded, manage the popup, move through options, and expose the selected value.
Form errors fail when developers render text beside a field without connecting the two. Associate the message and state explicitly:
<label for="email">Email address</label>
<input id="email" aria-invalid="true" aria-describedby="email-error">
<p id="email-error">Enter a valid email address.</p>
Silent updates and escaped focus
Loading indicators, empty autocomplete results, and success messages need a communication plan. A status region can announce a non-blocking update, while an actionable failure may require focus on the relevant control. Don’t use a live region as a substitute for a missing label or a logical reading order.
GDS monitoring has identified lack of visible focus as a recurring public-sector problem, affecting keyboard and screen reader users. A control can be technically focusable and still unusable if its focus indicator blends into the surrounding interface. Test focus after opening menus, submitting forms, moving through tabs, and closing overlays, not only on the initial page load.
Finally, check multi-step forms. Each step needs a clear heading, an announced progress context, and an error route that doesn’t force users to rediscover the page. A screen reader user should know what changed, what remains required, and where the next action is.
Building a Repeatable Accessibility Workflow
Accessibility becomes manageable when teams treat it as a component contract and a release practice, not a specialist review at the end. Define each component’s role, accessible name, states, keyboard model, focus entry point, focus exit point, error behaviour, and announcement requirements alongside its visual specification.
Run automated checks early. Integrate axe-core or an equivalent rule set into component and page tests, then use Lighthouse as a supplementary signal during development. These tools are good at catching structural defects, but they won’t validate the experience of selecting an option, recovering from an error, or completing a multi-step flow with a screen reader.
A workflow that survives release pressure
Use a layered process:
- Component checks: Inspect the accessibility tree, verify names and states, and exercise keyboard paths before a component enters the shared library.
- Task checks: Run realistic flows with NVDA, VoiceOver, and TalkBack across the supported browser matrix.
- Regression checks: Keep a short scripted path for dialogs, dropdowns, comboboxes, tabs, toasts, and form errors.
- Operational checks: Monitor focus indicators, dynamic announcements, and complex-content alternatives after release.
Document exceptions openly. If a survey tool can’t provide an accessible ranking interaction, plan the alternative version before publication and define how the responses will be combined. The Glasgow guidance describes this kind of parallel accessible approach, which is more honest and useful than claiming generic ARIA has solved an incompatible question type.
GOV.UK’s DWP accessibility baseline links screen reader support to the Public Sector Bodies Accessibility Regulations 2018 and states that users should be able to listen using recent JAWS, NVDA, and VoiceOver versions. The DWP accessibility statement illustrates the operational standard UK teams need to maintain. An accessibility audit workflow can help organise automated findings, manual evidence, ownership, and follow-up without treating the audit report as the finish line.
Shared headless primitives can support this process by centralising keyboard handling, ARIA relationships, and focus restoration. Teams still need to validate the complete application, because correct component behaviour can be undermined by surrounding layout, state updates, or an inaccessible content model. The durable approach combines reusable implementation patterns with repeatable human testing.
DOM Studio provides headless web component primitives and Vue integrations for menus, listboxes, dialogs, drawers, tabs, comboboxes, toasts, and related controls, with built-in ARIA and focus behaviour. Visit DOM Studio to inspect the components, embedded documentation, and AI-editable specifications before adding another custom interaction to your application.
