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; arole="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
<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.
Related terms
Help us improve this glossary term
Scan your site
Rocket Validator scans thousands of pages in seconds, detecting accessibility and HTML issues across your entire site.