# ARIA Widget Patterns

> Canonical HTML version: https://rocketvalidator.com/glossary/aria-widget-patterns
> Attribution: Rocket Validator (https://rocketvalidator.com)
> License: CC BY 4.0 (https://creativecommons.org/licenses/by/4.0/)

ARIA widget patterns are standardized interaction models defined by the WAI-ARIA Authoring Practices that describe the expected keyboard behavior, focus management, and ARIA roles, states, and properties for custom interactive UI components like tabs, menus, dialogs, and tree views.

When native HTML elements like `<button>`, `<select>`, or `<input>` don't meet a design requirement, developers build custom widgets using `<div>`, `<span>`, and other generic elements. These custom components lack the built-in semantics and keyboard behavior that assistive technologies and users depend on. ARIA widget patterns bridge that gap by providing a blueprint for how each type of widget should be constructed—specifying which ARIA roles to apply, which states and properties to manage, how keyboard focus should move, and what keys should trigger specific actions.

The WAI-ARIA Authoring Practices Guide (APG), maintained by the W3C, catalogs dozens of these patterns. Each pattern documents the DOM structure, required ARIA attributes, and a complete keyboard interaction model. Following these patterns ensures that a custom widget behaves consistently across screen readers, browsers, and operating systems, giving users the same experience they would have with a native control.

## Why ARIA widget patterns matter

Custom widgets that ignore established patterns create serious barriers. A screen reader user encountering a tab interface built from plain `<div>` elements with no `role`, no `aria-selected` state, and no arrow-key navigation will have no idea that tabs exist, which tab is active, or how to switch between them. Sighted keyboard users will face similar confusion if Tab is the only way to navigate between options rather than the expected arrow keys.

ARIA widget patterns matter because they:

- **Set user expectations.** Assistive technology users learn a finite set of interaction conventions. A `role="menu"` is navigated with arrow keys; a `role="dialog"` traps focus. Consistent behavior reduces cognitive load.
- **Enable interoperability.** Screen readers map ARIA roles to platform accessibility APIs. Without the correct role and state, the widget is invisible or misleading.
- **Improve keyboard accessibility.** Each pattern defines which keys perform which actions, ensuring the widget is fully operable without a mouse.
- **Prevent misuse of ARIA.** Applying a role without implementing the corresponding keyboard behavior is worse than using no ARIA at all, because it promises functionality that doesn't exist.

Developers, designers, and QA testers all benefit from understanding these patterns. Validators and automated accessibility tools can flag missing roles and states, but only manual testing confirms that the keyboard interaction model is correctly implemented.

## How ARIA widget patterns work

### Roles, states, and properties

Every pattern starts with a set of ARIA roles that establish the widget's identity. A tabbed interface, for example, uses `role="tablist"` on the container, `role="tab"` on each tab, and `role="tabpanel"` on each panel. States like `aria-selected="true"` communicate which tab is active, and properties like `aria-controls` link a tab to its panel.

### Keyboard interaction model

Each pattern prescribes specific keystrokes. For a tablist, the Left and Right arrow keys move focus between tabs, Home jumps to the first tab, End jumps to the last, and Tab moves focus into the active panel. This roving `tabindex` technique keeps the tab order clean—only one tab sits in the natural tab sequence at a time.

### Focus management

Patterns define whether focus should rove among child items (as in a toolbar or tablist), be trapped within a container (as in a modal dialog), or follow a composite widget model (as in a grid). Proper focus management relies on programmatically setting `tabindex="0"` on the active item and `tabindex="-1"` on all others, then moving focus with `element.focus()` in response to keystrokes.

### Common patterns

Some of the most frequently implemented ARIA widget patterns include:

- **Dialog (modal)** — `role="dialog"`, `aria-modal="true"`, focus trap, Escape to close.
- **Tabs** — `role="tablist"`, `role="tab"`, `role="tabpanel"`, arrow-key navigation.
- **Menu / Menubar** — `role="menu"`, `role="menuitem"`, arrow keys plus type-ahead.
- **Combobox** — `role="combobox"`, `aria-expanded`, `aria-activedescendant`, associated listbox.
- **Tree view** — `role="tree"`, `role="treeitem"`, arrow keys to expand/collapse.

## Code examples

### Bad example — tabs without ARIA or keyboard support

```html
<div class="tabs">
  <div class="tab active" onclick="showPanel(0)">Account</div>
  <div class="tab" onclick="showPanel(1)">Security</div>
</div>
<div class="panel" id="panel-0">Account settings…</div>
<div class="panel" id="panel-1" style="display:none;">Security settings…</div>
```

This implementation has no roles, no states, no keyboard interaction, and the `<div>` elements are not focusable. A screen reader announces them as generic text, and keyboard users cannot activate or navigate them.

### Good example — tabs following the ARIA widget pattern

```html
<div role="tablist" aria-label="Settings">
  <button
    role="tab"
    id="tab-account"
    aria-selected="true"
    aria-controls="panel-account"
    tabindex="0">
    Account
  </button>
  <button
    role="tab"
    id="tab-security"
    aria-selected="false"
    aria-controls="panel-security"
    tabindex="-1">
    Security
  </button>
</div>

<div
  role="tabpanel"
  id="panel-account"
  aria-labelledby="tab-account"
  tabindex="0">
  Account settings…
</div>

<div
  role="tabpanel"
  id="panel-security"
  aria-labelledby="tab-security"
  tabindex="0"
  hidden>
  Security settings…
</div>

<script>
  const tabs = document.querySelectorAll('[role="tab"]');
  const panels = document.querySelectorAll('[role="tabpanel"]');

  tabs.forEach((tab) => {
    tab.addEventListener("keydown", (e) => {
      const index = Array.from(tabs).indexOf(e.currentTarget);
      let newIndex;

      if (e.key === "ArrowRight") {
        newIndex = (index + 1) % tabs.length;
      } else if (e.key === "ArrowLeft") {
        newIndex = (index - 1 + tabs.length) % tabs.length;
      } else if (e.key === "Home") {
        newIndex = 0;
      } else if (e.key === "End") {
        newIndex = tabs.length - 1;
      } else {
        return;
      }

      e.preventDefault();
      activateTab(tabs[newIndex]);
    });

    tab.addEventListener("click", () => activateTab(tab));
  });

  function activateTab(newTab) {
    tabs.forEach((t) => {
      t.setAttribute("aria-selected", "false");
      t.setAttribute("tabindex", "-1");
    });
    panels.forEach((p) => p.setAttribute("hidden", ""));

    newTab.setAttribute("aria-selected", "true");
    newTab.setAttribute("tabindex", "0");
    newTab.focus();

    const panel = document.getElementById(newTab.getAttribute("aria-controls"));
    panel.removeAttribute("hidden");
  }
</script>
```

In this corrected example, each tab uses `role="tab"` inside a `role="tablist"`, the active tab has `aria-selected="true"`, panels are linked via `aria-controls` and `aria-labelledby`, and arrow keys handle navigation with roving `tabindex`. Screen readers announce "Account tab, selected, one of two" and keyboard users can move between tabs without extra Tab stops. This is exactly the kind of predictable, well-documented behavior that ARIA widget patterns are designed to guarantee.
