A web interface is not only the pixels painted in a viewport. Browsers also expose a structured description of that interface to operating-system accessibility APIs. Screen readers, voice-control software, switch devices, and browser automation can use this accessibility tree to discover controls, understand relationships, and operate the page without depending on its visual arrangement.
That tree is derived rather than authored directly. HTML semantics provide the strongest input, while text, labels, ARIA attributes, visibility, CSS, and current UI state refine the result. A page can therefore look correct while exposing a confusing or empty interface. It can also contain apparently rich ARIA markup while remaining unusable because its keyboard behavior does not match the roles it claims.
The practical thesis is that accessibility starts with honest HTML. Native elements bundle a role, expected state, keyboard behavior, focus rules, and platform mappings into one tested contract. ARIA is useful for filling genuine semantic gaps, but it does not manufacture behavior. Designing and testing the accessibility tree as a public interface makes that distinction concrete.
Treat the Accessibility Tree as an Interface
The DOM records nodes and attributes. The visual rendering tree determines what is painted. The accessibility tree answers a different set of questions: what meaningful objects exist, what is each object’s role and accessible name, what state is it in, and how is it related to other objects?
A browser roughly combines several inputs to answer them:
- Native HTML semantics, such as
button,nav,main,input, and heading levels. - Text content and explicit labeling relationships.
- Current properties such as checked, expanded, selected, invalid, or disabled.
- ARIA roles, states, and relationships where HTML cannot express the widget.
- Rendering and visibility information that determines whether content is exposed.
The resulting nodes are mapped into a platform API such as UI Automation on Windows or AX APIs on Apple platforms. Assistive technology queries that API rather than parsing the page as ordinary source text. Exact mappings differ by browser, operating system, and assistive technology, so the accessibility tree is not a byte-for-byte portable artifact. Its contract is behavioral: users should encounter equivalent roles, names, states, relationships, and operations.
Not every DOM node needs its own accessible node. A decorative wrapper normally contributes nothing. Text can be folded into an ancestor’s name. Elements hidden with hidden, display: none, or visibility: hidden are normally absent, while visually clipped content can remain available. An element with aria-hidden="true" is deliberately removed from the accessibility API even if it is painted. Putting focusable descendants inside such an element creates a contradiction: keyboard users can reach something that a screen reader has been told does not exist.
This derived nature explains why inspecting source alone is insufficient. Browser developer tools can show the computed role, name, properties, and accessibility-tree position. That view is the semantic equivalent of inspecting computed CSS: it reveals what the browser resolved, not merely what the author intended.
Prefer Native Elements Because They Bundle Contracts
Semantic HTML is often described as using meaningful tags, but its value is more operational than literary. A native <button> is focusable in the normal tab order, activates with expected keyboard commands, exposes a button role, participates in forms, reports disabled state, and receives established browser behavior. A <div class="button"> has none of those properties until code rebuilds them.
The same principle applies across the document:
| Intent | Native starting point | Contract it supplies |
|---|---|---|
| Navigate to a resource | <a href="..."> |
Link role, destination, keyboard activation, link actions |
| Trigger an action | <button> |
Button role, focus, Enter/Space activation, disabled behavior |
| Choose one option | Radio inputs in a <fieldset> |
Group name, mutual exclusion, checked state, keyboard conventions |
| Enter identified data | <label> with an input |
Programmatic name and a larger pointer target |
| Mark page structure | <header>, <nav>, <main>, <aside>, <footer> |
Landmarks for structural navigation |
| Organize content | Ordered headings and lists | Navigable hierarchy and item boundaries |
| Present tabular relationships | <table> with header cells |
Row and column associations |
Choose elements by behavior, not appearance. A link styled as a prominent rectangle remains a link if it changes location. A quiet icon-only control remains a button if it changes local state. CSS can make either look appropriate without falsifying its purpose.
Document structure matters because many screen-reader users navigate by landmarks and headings instead of reading every node in DOM order. One descriptive <main>, distinct navigation regions, and a coherent heading outline create useful shortcuts. Heading levels should represent nesting, not a desired font size. Skipping from an h2 to an h4 does not always make content impossible to understand, but it signals a hierarchy that is harder to predict and maintain.
Native semantics do not guarantee good design. A button named “Click here” is still vague, and a correctly marked table can still be overwhelming. They establish a sound mechanical baseline on which clear language, sensible grouping, and interaction design can operate.
Understand Accessible Names and Descriptions
A role tells a user what an object is. Its accessible name tells them which object it is. A screen reader might announce “Email address, edit” or “Save draft, button” because the browser computed a name and role from several possible sources.
For a text input, an associated <label> is the preferred name. For a button, visible text often supplies it. An image uses meaningful alternative text when the image conveys content, while a decorative image uses an empty alt so it does not add noise. aria-labelledby can reference visible text elsewhere, and aria-label can supply a string when no visible label is practical. The full accessible-name algorithm has role-specific rules and precedence; the engineering lesson is not to memorize every branch, but to avoid competing sources.
If a button displays “Delete report” but has aria-label="Remove", many assistive technologies announce only “Remove.” Voice-control users who say the visible phrase may not be able to target it. Prefer the visible text as the accessible name, or ensure an additional name starts with the visible label. Do not add an aria-label merely because a control already has understandable text.
Descriptions supplement names rather than replacing them. aria-describedby is appropriate for persistent help, format constraints, or an error explanation. A required email field might have the name “Work email” and a description “Use the address managed by your organization.” Repeating the name in the description produces verbosity without new information.
Names must also survive localization and dynamic state. An icon’s filename is not a product label. Placeholder text is not a durable input label because it disappears while typing and may have weak contrast. Tests should assert the user-facing computed name, not just the presence of some attribute. That keeps refactors focused on the semantic outcome.
Make Focus and Keyboard Behavior Match the Semantics
The accessibility tree describes possible operations, but keyboard interaction proves whether they are real. Native interactive elements enter the sequential focus order automatically. Pressing Tab should move through operable controls in a logical sequence that broadly follows the DOM. Visible focus must remain obvious against every background and state.
Avoid positive tabindex values. They create a second hand-maintained order that diverges as the page changes. tabindex="0" places a custom interactive element in normal order, while tabindex="-1" allows programmatic focus without adding a Tab stop. Neither gives a <div> button activation behavior, a role, or disabled semantics.
Composite widgets such as tab lists, menus, and listboxes usually have one Tab stop and use arrow keys internally. This is called roving focus or can be expressed with an active descendant pattern. Such widgets should follow established keyboard conventions; inventing a different key map makes a familiar announced role misleading. For ordinary groups of links or buttons, let each native element remain a normal Tab stop instead of adding composite-widget complexity.
Focus should move only for a user-centered reason. Opening a modal dialog moves focus inside it, constrains interaction to that dialog, and returns focus to the invoking control when it closes. Adding an item to a list usually should not steal focus from the Add button; a status announcement may be enough. Removing the currently focused item requires a deterministic next destination, such as the next item, previous item, or section heading.
CSS reordering can make visual and keyboard order disagree. Grid and flexbox can place the third DOM item first on screen, while Tab and screen-reader reading order still follow the DOM. Use source order to express the meaningful sequence, then use layout for arrangements that do not change that sequence.
Work Through a Semantic Shipping Selector
Consider a checkout that offers Standard, Express, and Pickup shipping. A visual implementation might render three clickable <div> cards, toggle a border on click, and store the selected text. It does not announce a group, expose which option is selected, support arrow-key radio behavior, or associate the explanatory text with each choice.
The native model is a radio group because exactly one option can be selected:
<fieldset>
<legend>Shipping method</legend>
<div class="shipping-option">
<input
id="shipping-standard"
type="radio"
name="shipping"
value="standard"
checked
aria-describedby="shipping-standard-detail"
/>
<label for="shipping-standard">Standard</label>
<span id="shipping-standard-detail">Arrives in 4 to 6 business days</span>
</div>
<div class="shipping-option">
<input
id="shipping-express"
type="radio"
name="shipping"
value="express"
aria-describedby="shipping-express-detail"
/>
<label for="shipping-express">Express</label>
<span id="shipping-express-detail">Arrives in 1 to 2 business days</span>
</div>
<div class="shipping-option">
<input
id="shipping-pickup"
type="radio"
name="shipping"
value="pickup"
aria-describedby="shipping-pickup-detail"
/>
<label for="shipping-pickup">Store pickup</label>
<span id="shipping-pickup-detail">Choose a store after continuing</span>
</div>
</fieldset>
<p id="shipping-status" role="status" aria-atomic="true"></p>
CSS can make each label and input container look like a selectable card. The browser still exposes a named “Shipping method” group, three radios, their names and descriptions, and the checked state. Tab reaches the radio group; arrow keys change selection according to platform conventions. Clicking a label activates its input, increasing the practical target without custom pointer code.
When selection changes, application code recalculates the order total. The visible total should be ordinary text. If users need immediate confirmation, update the existing role="status" node with a concise message such as “Express selected. Order total 42 dollars.” Reusing one status region avoids injecting multiple live regions. The form submission reads the checked input value, so visual state and submitted state have one source of truth.
Test the example from outcomes. Its computed group name is “Shipping method.” Exactly one radio is checked. The labels activate the corresponding controls. Arrow-key changes update both checked state and the visible styling. The status is announced once, and submitting without JavaScript still sends a valid shipping value. Those assertions are stronger than checking for a particular class name.
Use ARIA to Express Missing Semantics, Not Missing Behavior
ARIA can add or refine a role, state, property, or relationship in the accessibility tree. It cannot add keyboard listeners, focus management, form submission, validation, or visual styling. role="button" makes a node announce as a button but does not make Space activate it. aria-expanded="true" reports a state but does not reveal a panel.
Three rules keep ARIA disciplined:
- Use a native element when it already provides the required semantics and behavior.
- Change ARIA state in the same transaction as the visible and functional state.
- Do not override strong native semantics unless implementing a documented pattern that requires it.
An accordion trigger is normally a <button aria-expanded="false" aria-controls="panel-id"> inside a heading. Activation toggles both the panel’s hidden state and aria-expanded. If one update succeeds without the other, sighted and screen-reader users receive different truths. A component test should therefore assert them together.
Be especially cautious with role="application", broad aria-hidden, and custom widgets composed from generic elements. They can replace useful browser and screen-reader behavior across a large subtree. Complexity is justified only when a native composition cannot represent the interaction and the team can implement, document, and test the complete pattern.
Announce Dynamic Changes Without Creating Noise
Single-page interfaces change after initial load, but not every DOM mutation deserves an announcement. A live region tells assistive technology that updates within a stable node may be important. role="status" is generally polite and waits for the current announcement; role="alert" is assertive and should be reserved for urgent information.
Create the live-region container before the update when possible, then change its text. Keep messages short and actionable. Loading spinners should expose a meaningful busy state or status, but rapid progress ticks can overwhelm users. Announce milestones rather than every percentage. When a form fails, connect each invalid field to its error text, move focus to an error summary only when that helps navigation, and preserve entered values.
Route changes need an explicit focus policy because client-side navigation does not perform a browser document load. Update the document title, then move focus to the new page’s main heading or main container when navigation represents a new view. A filtering interaction within the same view usually retains focus on the filter control and reports the result count instead.
Virtualized lists introduce a deeper tradeoff. Removing off-screen items can make the accessible collection appear shorter or cause a focused node to disappear. Test with assistive technology and consider whether the performance benefit justifies the semantic discontinuity. A simple paginated list with accurate navigation may be more usable than an infinitely recycled DOM.
Verify the Contract at Several Layers
No single accessibility tool is an oracle. Static analyzers quickly catch missing labels, invalid ARIA values, duplicate IDs, and some contrast problems, but they cannot decide whether focus moves sensibly or instructions are understandable. Build a layered verification loop.
Start with semantic review during implementation. Inspect the browser’s accessibility pane and confirm each important object’s computed role, name, state, and relationships. Remove unnecessary ARIA and verify the result improves or stays equivalent. Review the page by headings and landmarks, not only visually.
Then exercise keyboard behavior without a pointer:
- Tab forward and backward through the complete workflow.
- Confirm focus order follows the meaningful reading order.
- Operate buttons, links, radios, selects, and custom composites with expected keys.
- Open and close dialogs, then verify focus containment and restoration.
- Trigger validation, loading, success, and destructive-action states.
- Confirm every focused element remains visible and unobscured.
Automated browser tests should query by semantic role and accessible name where possible. A test that selects getByRole('radio', { name: 'Express' }) depends on the same public contract a user receives. Assert state transitions such as checked, expanded, disabled, and invalid. Add an automated accessibility scan, but treat a clean report as the beginning of manual verification, not proof of conformance.
Finally, run representative workflows with screen readers and browser combinations supported by the product. Listen to reading order, labels, group context, state changes, errors, and status announcements. Include zoom, reflow, high contrast or forced colors, and reduced motion in the broader accessibility matrix. Recruit disabled users for usability research when product risk warrants it; standards checks cannot reveal every mismatch between a workflow and real strategies.
Keep Semantic Regressions Operationally Visible
Accessibility failures often enter through ordinary refactors: a button becomes a clickable wrapper, a visible label is replaced by an icon, a modal loses focus restoration, or a loading rewrite removes its status message. Protect semantic outcomes in component tests and critical end-to-end journeys. Code review templates can ask for the expected role, name, keyboard behavior, and focus effect of every new interaction.
Watch for recurring failure modes. Redundant ARIA can override correct native output. Positive tabindex makes order brittle. Hidden focusable elements create unreachable or unnamed stops. Duplicate names make repeated controls indistinguishable. Live regions announce too much. Snapshotting raw accessibility trees across browser upgrades can also be noisy, so prefer targeted assertions about user-relevant nodes and behaviors.
There are legitimate tradeoffs. A sophisticated custom editor or data grid may require semantics that native HTML cannot provide directly. Its cost includes keyboard conventions, focus algorithms, selection reporting, assistive-technology testing, and long-term maintenance. Make that cost explicit in the design decision. For common forms, navigation, disclosure, selection, and actions, native composition is usually both more robust and less code.
The accessibility tree is a production interface even though many developers never see it. Semantic HTML gives that interface truthful defaults; accessible names identify its objects; keyboard and focus behavior make its operations real; ARIA fills narrow gaps; and layered tests verify the computed result. When those pieces agree, the visual page and the nonvisual interface describe the same product rather than two loosely related implementations.