Learning Track
Filter content by your role to see the most relevant sections
Assistive Technology Support
Support screen readers and assistive tools properly: the name/role/value tree they consume, keyboard navigation as the operability backbone, semantics in component contracts, and the manual AT passes automation cannot replace.
Why This Matters
Assistive technologies are the users your interface cannot see. A screen reader does not experience your carefully composed layout—it experiences an accessibility tree of names, roles, and values, linearized in DOM order, announced one node at a time. A keyboard user does not experience your hover states—they experience a tab order and whatever focus tells them. Designing for the visible interface while ignoring the tree is designing a beautiful building with no doors.
For a design system the stakes compound: every semantic mistake ships inside a component and multiplies across every screen that uses it. The inverse is equally true—get the semantics right in the component once, and every consumer inherits correct behavior without reading a line of ARIA documentation. That is the leverage this page is about.
Core Concepts: The Tree, The Order, The Names
The Accessibility Tree Is the Real Interface
Browsers derive an accessibility tree from the DOM—each node exposing a name, a role, a value, and states. AT consumes that tree, not your pixels. The system-level consequence: components are where the tree is either well-formed or broken, which is why the contract files carry semantics as fields:
// From any ui/components/*/[Name].contract.json
"a11y": {
"role": "generic", // the tree node's role
"labeling": [], // how the name is provided
"keyboard": [], // expected keyboard behavior
"apgPattern": null // ARIA Authoring Practices pattern
}Declared semantics are reviewable semantics: a component whose contract says role: "button" but whose keyboard list is empty has a visible hole, before any screen reader run. The fields are also testable surface—the tooling page's audits read them as expectations.
Names: Every Control Answers "What Am I?"
The accessible name is the tree's primary key. The system's smallest complete example is the Icon component's binary:
// ui/components/Icon/Icon.tsx (behavior)
label provided → role="img" + aria-label={label} // meaningful
label omitted → aria-hidden="true" // decorative
// Meaningful: the icon IS the affordance
<Icon icon={faXmark} label="Close" />
// Decorative: adjacent text carries the meaning
<button><Icon icon={faStar} /> Favorite</button>The default is silence, and the default is correct—in a well-composed interface most icons sit beside text that already names the thing. The failure modes are symmetric and silent: unlabeled meaningful icons vanish from the tree; labeled decorative icons announce noise ("star… Favorite"). Names are also a quality surface: a name is a name ("Close"), not a description ("gray x icon top right").
Order: DOM Is Reading Order, Layout Is Styling
The tree is linear; screens are not. The system rule that reconciles them: DOM order equals reading order, and visual arrangement is achieved with flow and grid placement—never by re-ordering the DOM to match a picture. The search-results pattern from the grid page is the canonical case: filters come after results in the DOM (screen reader users hit content first) and are placed visually with grid placement. Break the rule and two users of the same screen experience different products—the sighted one and everyone else.
Keyboard: The Operability Backbone
- Everything interactive is reachable by Tab and arrow keys—the native elements give this free, which is most of the native-first argument.
- Focus is always visible—the composed focus ring from the token page, never
outline: nonewithout an equal-or-better replacement. - Overlays manage focus—dialogs trap focus while open and return it to the trigger on close; menus move focus into themselves and restore it. This is contract material (the
keyboardlist), not per-usage discretion. - Keyboard is the test substrate—the e2e suite drives the product the way a keyboard user does; a component that cannot be driven by keyboard cannot even be tested.
The Wider AT Ecosystem
Screen readers (VoiceOver, NVDA, JAWS, TalkBack) are the most cited consumers, but the tree serves all of it: switch access, voice control (which addresses controls by name—the naming discipline above, doubly load-bearing), magnification (which turns your responsive layout into a panning interface—the reflow guarantee), and reading modes that strip your CSS entirely (leaving exactly your semantics and your order). The unifying observation: every AT is a tree consumer. Serve the tree, and the ecosystem follows.
System Roles: Who Owns the Tree
Engineering Impact
Engineers own tree correctness in components: native elements first, ARIA only for what native cannot express, contracts filled honestly, focus management as specified behavior. The review question is always available: "what does the tree say?"
Design Impact
Designers own the names and the order: the accessible names that appear on controls (they are the designer's words, three surfaces—tree, tooltip, aria-label), and reading order as a design decision the DOM then honors.
Accessibility Impact
The a11y function owns the manual passes automation cannot replace: real screen reader walks on the real patterns, on the real browsers, with the real complaints filed as contract and test gaps rather than as one-off fixes.
Design & Code Interplay
The design-side expression of the tree: comps annotated with names (what each control announces), order (the numbered reading sequence), and meaning-status for every icon. It is fifteen minutes per screen, and it converts accessibility from an engineering surprise into a design deliverable—because the annotations are exactly the contract fields the component needs filled.
Code-side, the rules as the component patterns enforce them:
// Native first — the tree comes free
<button onClick={save}>Save</button>
// name: "Save" · role: button · operable · focusable · done
// ARIA only where native cannot express the pattern
<div role="tablist">
<button role="tab" aria-selected="true" aria-controls="panel-1">…</button>
</div>
// and the contract's apgPattern names the reference implementation
// Focus management as specified behavior, not discretion
function Dialog({ onClose }) {
// trap focus while open; return to trigger on close
// → contract a11y.keyboard lists Tab cycling + Escape
}Applied Example: The Screen Reader Pass, as Protocol
The manual pass that automation cannot replace, run as a repeatable protocol rather than a vibe:
- Pick the pattern, not the page: one component at a time—the Dialog, the Select, the Tabs—in a real screen, on VoiceOver or NVDA.
- Navigate as two users: by Tab/arrow (structure) and by the screen reader's roster (landmarks, headings, controls). Both must make sense independently.
- Transcribe what you hear: the announcement stream, verbatim. "star, Favorite, button" is a finding, not a paraphrase.
- File gaps where they live: missing name → component contract; wrong order → DOM/page structure; trap failure → keyboard behavior spec; noise → the decorative default missed. Every finding lands in a layer, or it lands nowhere.
- Add the regression check: what axe or the e2e walk can assert gets asserted, so the next pass hunts only what automation cannot see.
Constraints & Trade-offs
- Native-first vs full ARIA control: native elements inherit a decade of browser and AT bugfixes; bespoke ARIA re-implements them with none. The cost of native is styling discipline, which the design system exists to pay.
- Manual passes vs automation: the pass is slow, unscalable, and irreplaceable—the computable subset automates; comprehension and announcement quality do not. Budget both or have neither.
- Announcement verbosity: richer ARIA labeling can over-describe (polite live regions become chatty). Name quality is editorial work with a11y consequences.
Common Pitfalls & Failure Modes
1. Divs with event handlers
// Bad — Invisible to the tree: no role, no name, no keys
<div onClick={submit}>Submit</div>
// Good
<button onClick={submit}>Submit</button>2. Outline removed, nothing added
/* Bad: Keyboard users lose their cursor */
.card:focus { outline: none; }
/* Good: Never remove without replacing — and the ring token exists */3. Visual order ≠ DOM order
Re-ordered DOM matching a picture breaks every tree consumer's mental model; grid placement is styling and costs nothing.
4. Focus left stranded after overlays close
The dialog closes and focus snaps to the page start—the keyboard user's context is destroyed. Return to the trigger; it is contract behavior.
5. Testing only with automation
Green axe on a tree that announces nonsense is green nonsense. The manual pass is the only check for announcement quality.
Verification Checklist
Additional Resources
- ARIA Authoring Practices Guide — the pattern reference the contracts cite (
https://www.w3.org/WAI/ARIA/apg/) - Accessibility tooling — what the automation layer can and cannot see (
/blueprints/foundations/accessibility/tooling) - Component standards — the contract anatomy these semantics live in (
/blueprints/component-standards/anatomy)
Related Concepts
Reflection Questions
Apply This Concept
Run the announcement-transcription protocol on one pattern you own. Write the verbatim stream, mark each line good/noise/missing, and name the layer each finding belongs to.
Reflect
Voice control addresses controls by their accessible names. What does that fact alone imply about name collisions across a screen, and where should the rule live?
