aria-authoring-patterns · git:20260724.d4db756 · 2026-07-24 · sha256 e2159fcd56870e5d

aria-authoring-patterns git:20260724.d4db756A

Immutable. This exact content never changes and is served at /api/v1/blob/e2159fcd56870e5d.

---
name: aria-authoring-patterns
description: "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][apg]).

[apg]: https://www.w3.org/WAI/ARIA/apg/patterns/

The APG documents 31 patterns; each one specifies:

- The required `role` attribute.
- Required and optional `aria-*` states (e.g. `aria-expanded`,
  `aria-selected`, `aria-current`).
- The full keyboard-interaction model (Tab / Arrow keys / Enter /
  Escape / Home / End / Type-ahead).
- DOM structure conventions.
- Common pitfalls.

## When to use

- Authoring a custom widget (Combobox, Tabs, Tree, Carousel, etc.).
- Reviewing a third-party UI library's component for ARIA
  correctness.
- Adding ARIA to an existing widget that was hand-rolled without
  it.
- Configuring assertions in `axe-a11y` /
  `pa11y-a11y` to enforce per-pattern
  conventions.

## 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][apg]:

### 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

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

```html
<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)

```html
<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)

```html
<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. |

## References

- [apg-patterns][apg] - W3C ARIA Authoring Practices Guide
  (canonical patterns).
- WAI-ARIA 1.2 Spec - https://www.w3.org/TR/wai-aria-1.2/
- `wcag-focus-trap` - Dialog
  pattern's focus-management deep dive.
- `wcag-keyboard-navigation` - Keyboard-interaction conformance underlying every APG pattern.
- `axe-a11y`,
  `pa11y-a11y` - runners that detect
  ARIA misuse programmatically.