aria-authoring-patterns
Reference for the W3C ARIA Authoring Practices Guide (APG) - covers the 31 canonical interactive-widget patterns (Combobox, Dialog, Menu, Tabs, Tree, etc.), their required ARIA roles and states, the keyboard-interaction model per pattern, and the canonical-violations to watch for. Use when authoring a custom interactive widget that doesn't have a native HTML equivalent, or when reviewing one for ARIA correctness.
aria-authoring-patterns
Overview
The W3C ARIA Authoring Practices Guide (APG) is the canonical reference for how to build accessible custom widgets - patterns where native HTML doesn't have a single matching element (apg-patterns).
The APG documents 31 patterns; each one specifies:
When to use
The first rule of ARIA
The APG (and WAI more broadly) consistently emphasizes:
No ARIA is better than bad ARIA. Native HTML elements with built-in semantics are preferred over custom widgets whenever possible.
In practice:
| Native element | Don't replace with |
|---|---|
<button> | <div role="button"> - same semantics, more code, easier to break. |
<input type="checkbox"> | Custom checkbox unless visual constraints demand it. |
<a href> | <div onclick="navigate(...)">. |
<select> | Custom listbox UNLESS multi-select with rich content per option. |
<input type="radio"> | Custom radio group. |
When the visual design demands a custom widget, follow the matching APG pattern exactly.
Canonical patterns (APG)
The 31 patterns documented per apg-patterns:
Form controls
| Pattern | When to use | Native fallback |
|---|---|---|
| Button | Trigger an action. | <button> - almost always. |
| Checkbox | Two-state binary; or three-state (mixed). | <input type="checkbox">. |
| Combobox | Searchable / filterable select with autocomplete. | <select> for plain selection only. |
| Disclosure | Show/hide content; one-way reveal. | <details> / <summary>. |
| Listbox | Multi-select OR rich-content single-select. | <select> for plain. |
| Menu / Menubar | Application menus (File / Edit / View bar). | (No native; APG required.) |
| Menu Button | Button that opens a menu. | (No native; APG required.) |
| Radio Group | Mutually-exclusive option set. | <input type="radio" name="x">. |
| Slider | Single-value within a range. | <input type="range">. |
| Slider (Multi-Thumb) | Range selection. | (No native; APG required.) |
| Spinbutton | Numeric input with increment/decrement controls. | <input type="number">. |
| Switch | On/off toggle, semantically distinct from checkbox. | (No native; APG required.) |
Containers / navigation
| Pattern | When to use |
|---|---|
| Accordion | Vertically-stacked list of headers; each expands. |
| Breadcrumb | Hierarchical-location indicator. |
| Carousel | Cycle through groups of equivalent content. |
| Dialog (Modal) | Overlay requiring user interaction. (See wcag-focus-trap.) |
| Alert Dialog | Modal that interrupts to convey urgency. |
| Tabs | Switch between sibling content sections. |
| Toolbar | Group of controls (buttons, dropdowns). |
| Tree View | Hierarchical navigation. |
| Treegrid | Hierarchical data grid (table rows that expand). |
| Window Splitter | Resize between two panes. |
Feedback / indicators
| Pattern | When to use |
|---|---|
| Alert | Important programmatically-determined message. |
| Feed | Dynamic stream of articles. |
| Grid | 2D widget for navigating tabular UI elements. |
| Meter | Scalar measurement within a known range. |
| Tooltip | Brief contextual hint on hover/focus. |
Content
| Pattern | When to use |
|---|---|
| Landmarks | Navigation regions (<nav>, <main>, etc.). |
| Link | Navigation to a different resource. |
| Table | Tabular data (use <table>). |
Per-pattern essentials
The APG's per-pattern documentation is detailed; for each pattern this skill highlights the load-bearing essentials.
Combobox
<label for="combo">Choose a fruit</label>
<input
id="combo"
role="combobox"
aria-expanded="false"
aria-controls="combo-listbox"
aria-autocomplete="list"
aria-activedescendant=""
/>
<ul id="combo-listbox" role="listbox" hidden>
<li id="opt-1" role="option" aria-selected="false">Apple</li>
<li id="opt-2" role="option" aria-selected="false">Banana</li>
</ul>| Required ARIA | Why |
|---|---|
role="combobox" | Identifies the input as a combobox. |
aria-expanded | Tracks open/closed state. |
aria-controls | Links to the listbox ID. |
aria-autocomplete | none / inline / list / both. |
aria-activedescendant | Tracks the focused option without moving DOM focus. |
Tabs
<div role="tablist" aria-label="Settings sections">
<button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1" tabindex="0">General</button>
<button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1">Privacy</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">...</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>...</div>| Keyboard | Behavior |
|---|---|
| Tab | One tabstop for the whole tablist (only the active tab is tabindex="0"). |
| Left/Right arrow | Navigate between tabs. |
| Home / End | First / last tab. |
| Enter / Space | Activate (or auto on focus per implementation). |
Disclosure (Show/Hide)
<button aria-expanded="false" aria-controls="disclosure-1">More details</button>
<div id="disclosure-1" hidden>... content ...</div>The simplest pattern; canonical native equivalent is <details> / <summary> - use that unless the visual design demands the button form.
Tooltip (per SC 1.4.13)
<button aria-describedby="tt-1">Save</button>
<div id="tt-1" role="tooltip" hidden>Save the current document</div>| Behavior |
|---|
| Show on hover OR focus. |
| Dismissable via Esc (per SC 1.4.13). |
| Hoverable - user can move pointer to tooltip text. |
| Persistent until pointer / focus leaves OR Esc dismisses. |
(See wcag-color-contrast SC 1.4.13 for the three conditions.)
ARIA states reference
The most-used aria-* states across patterns:
| State | Use |
|---|---|
aria-expanded | Disclosure / Accordion / Combobox / Menu Button. |
aria-selected | Listbox / Tab. |
aria-checked | Checkbox / Radio. |
aria-pressed | Toggle button. |
aria-current | Current item in a set (page in pagination, item in breadcrumb). |
aria-controls | Element that this trigger controls. |
aria-labelledby | Reference to the labelling element. |
aria-describedby | Reference to a description element (tooltip, hint). |
aria-live | Live region; polite / assertive / off. |
aria-invalid | Form field validation state. |
aria-required | Form field required (with native required for forms). |
aria-disabled | Functionally disabled but in tab order (vs. disabled attribute which removes from tab order). |
aria-modal | Dialog modal flag; see wcag-focus-trap. |
Common ARIA failures
| Failure | Why | Fix |
|---|---|---|
<div role="button"> without keyboard handlers | Custom button needs tabindex="0" AND Enter/Space handlers. | Use <button> OR add both. |
aria-label on a <div> that has visible text label | Redundant; aria-label overrides the visible text for screen readers (becomes confusing). | Use aria-labelledby referencing the visible text element. |
aria-hidden="true" on a focusable element | Element is hidden from screen readers but still in tab order - broken state. | If you want to hide, also remove from tab order. |
Forgetting role="tab" / role="tabpanel" pairs | Screen reader doesn't announce the relationship. | Pair every tab with a panel via aria-controls / aria-labelledby. |
Hand-rolled combobox without aria-activedescendant | Arrow keys don't announce options. | Implement aria-activedescendant or move actual DOM focus. |
role="presentation" on a meaningful container | Removes semantics; sometimes used to "fix" a layout issue but breaks structure. | Don't override structure to fix layout; fix layout. |
Anti-patterns
| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Inventing a custom pattern not in the APG | No screen-reader convention; users can't form expectations. | Match an APG pattern exactly OR fall back to native HTML. |
Mixing native and ARIA semantics (<button role="link">) | Confusing - element behaves like a button but says it's a link. | One or the other; native is preferred. |
tabindex="3" to control sequence | Positive tabindex creates fragile order. | DOM order + tabindex="0" for custom focusables. |
Adding aria-label to every element "for accessibility" | If the element has a visible label, aria-label overrides it. Many elements don't need a label at all. | Only add aria-label when there's no visible text label AND the element is interactive. |