WAI-ARIA Compound Widgets: Building Accessible Comboboxes, Menus, and Dialogs from Scratch
Building complex UI widgets requires strict adherence to WAI-ARIA Authoring Practices (APG). This article constructs a complete WAI-ARIA 1.2 Combobox with list autocomplete, tabbed panels, and accessible dropdowns from scratch, managing dual-state synchronization, virtual pointer tracking, and React compound component architecture.
Frontend Accessibility Architecture: From Semantic DOM Foundations to Resilient Accessible State Engines
WAI-ARIA 1.2 Compound Widgets: APG-Compliant Combobox & Tablist
Part 3 built the FocusStateMachine — a useReducer-backed automaton that owns focus transitions, applies inert through a Focus Guard, and models roving tabindex as a COMPOSITE_ACTIVE state. The tension Part 3 left open: the state machine provides the focus infrastructure, but the individual compound widget types — combobox, tablist, tree, menu — each have their own rich keyboard interaction contracts specified by WAI-ARIA 1.2 and the APG. Implementing the state transitions correctly requires knowing exactly which aria-* attributes drive AT announcements, which keyboard events the APG mandates, and how the WAI-ARIA role model maps to AT behavior.
This article builds two production-grade compound widgets from first principles: an APG-compliant combobox with aria-autocomplete="list" (used as the Portal Canvas workspace search) and a manual-activation tablist (used as the Portal Canvas settings panel tabs). Both are built against the WAI-ARIA 1.2 spec, verified against the APG interaction model, and wired to the focus state machine from Part 3.
1. The ARIA Role Model — Owned Relationships and Required Properties
Before building widgets, engineers must understand how the WAI-ARIA role model constrains element relationships. Every composite widget role specifies:
- Required owned elements: element types that must be direct or descendant owners for the role to be valid
- Required context: parent role that must exist for the element's role to be valid
- Required states and properties: ARIA attributes that must be present for the role to work correctly
1.1 Role Ownership Constraints
Putting an role="option" element inside a <div> that does not have role="listbox" violates the required context constraint. Screen readers in some AT will refuse to announce the option's selected state because the required ancestor role is missing. Always satisfy the ownership hierarchy.
1.2 Required Properties Reference
| Role | Required Properties | Supported States | Notes |
|---|---|---|---|
combobox |
aria-expanded, aria-controls (popup ID) |
aria-disabled, aria-required |
WAI-ARIA 1.2: input IS the combobox |
option |
— | aria-selected, aria-disabled, aria-checked (multi-select) |
Must be owned by listbox |
tab |
— | aria-selected, aria-disabled |
Must be owned by tablist |
tabpanel |
aria-labelledby (pointing to its tab) |
— | Associates panel with its tab |
dialog |
aria-labelledby or aria-label |
aria-modal |
aria-modal affects which content AT presents |
alertdialog |
aria-labelledby + aria-describedby |
aria-modal |
Requires both title and description |
menu |
— | — | Owns menuitem, menuitemcheckbox, menuitemradio |
menuitem |
— | aria-disabled, aria-expanded (for submenu) |
Must be owned by menu/menubar |
2. The APG-Compliant Combobox
The combobox pattern is one of the most complex compound widgets in the APG. WAI-ARIA 1.2 revised the pattern significantly from 1.1 — the 1.1 pattern required a wrapper <div> with role="combobox" around the input; in 1.2, the <input> element itself is the combobox.
2.1 WAI-ARIA 1.2 Combobox Anatomy
The aria-activedescendant pattern is the correct way to move "virtual focus" within a listbox while keeping DOM focus on the combobox input. It tells the AT which option is currently highlighted. Never move actual DOM focus into the listbox options — this breaks the editing experience (the user can no longer type in the input).
2.2 Full Combobox Implementation
2.3 Screen Reader Announcement Trace
When a user types "work" and arrows to the second option:
3. The APG-Compliant Manual-Activation Tablist
The Portal Canvas settings panel uses a tablist to organize settings categories. The APG specifies two tablist activation models: Automatic (activating a tab immediately changes the panel) and Manual (activating a tab requires pressing Enter or Space). Manual activation is recommended when tab panel content is expensive to load.
3.1 Required ARIA Structure
Tab panels that are not currently active must be hidden using the hidden attribute (or display: none). aria-hidden="true" alone is insufficient — it hides from AT but leaves the panel in the tab order. The hidden attribute removes the panel from both the tab order and the AXTree.
3.2 Full Tablist Implementation
3.3 APG Keyboard Interaction Summary
| Key | Action (Manual Activation Tablist) |
|---|---|
→ Arrow |
Moves focus to next tab; wraps to first |
← Arrow |
Moves focus to previous tab; wraps to last |
Home |
Moves focus to first tab |
End |
Moves focus to last tab |
Enter / Space |
Activates focused tab (shows its panel) |
Tab |
Moves focus to the active tab panel |
Shift+Tab (from panel) |
Returns focus to the active tab |
In automatic activation mode, arrow keys both move focus AND activate the tab. This causes an accessibility problem when tab content loads asynchronously: the user might arrow through multiple tabs and trigger multiple expensive loads. Only use automatic activation for tabs with locally-available content. Use manual activation for any tab whose panel content involves a network request.
4. ARIA Live Region Integration for Widget Feedback
Compound widgets that perform asynchronous operations need to communicate status to screen reader users. Live regions (covered in full in Part 5) are introduced here for their widget-specific applications.
Summary
| Concept | Rule |
|---|---|
WAI-ARIA 1.2 combobox: the <input> is the combobox; virtual focus moves via aria-activedescendant; DOM focus stays on the input throughout the interaction. |
Never move DOM focus into the listbox — use aria-activedescendant to communicate the highlighted option. |
Tablist manual activation: arrow keys move focus only; Enter/Space activates; a separate focusedIndex tracks roving tabindex independent of selectedIndex. |
Decouple focus position from selection state in manual-activation tablists. |
ARIA ownership hierarchy must be satisfied: role="option" must be owned by role="listbox"; role="tab" must be owned by role="tablist". |
Violations cause AT to not announce required states — test against the AXTree, not just visual output. |
Inactive tab panels must be hidden with the hidden attribute, not just aria-hidden="true", to remove them from both the AXTree and the tab order. |
Use hidden (or display: none) to suppress inactive panels; aria-hidden alone leaves them keyboard-navigable. |
What's Next
Part 5 builds the live region system: the difference between
aria-live="polite"andaria-live="assertive", why a single global announcer avoids duplicate announcements, how to architect a streaming content announcer for the Portal Canvas activity feed that doesn't interrupt the user's current reading position, and the failure modes of poorly-timed live region injections.
This article was developed with AI-assisted deep search, specification cross-referencing, and technical research synthesis.