Skip to main content
Accessibility

ARIA Widget Patterns

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.
  • ARIA
  • design patterns
  • custom widgets
  • authoring practices
  • interactive components

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.
  • Tabsrole="tablist", role="tab", role="tabpanel", arrow-key navigation.
  • Menu / Menubarrole="menu", role="menuitem", arrow keys plus type-ahead.
  • Comboboxrole="combobox", aria-expanded, aria-activedescendant, associated listbox.
  • Tree viewrole="tree", role="treeitem", arrow keys to expand/collapse.

Code examples

Bad example — tabs without ARIA or keyboard support

<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

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

Help us improve this glossary term

Was this guide helpful?

Scan your site

Rocket Validator scans thousands of pages in seconds, detecting accessibility and HTML issues across your entire site.

🌍 Trusted by teams worldwide

Validate at scale.
Ship accessible websites, faster.

Automated HTML & accessibility validation for large sites. Check thousands of pages against WCAG guidelines and W3C standards in minutes, not days.

Scheduled Reports
API Access
Open Source Standards
$7 / 7 days

Pro Trial

Full Pro access. Cancel anytime.

Start Pro Trial →

Join teams across 40+ countries