The composite widgets
Comboboxes, listboxes, tree views, grids — the patterns where most teams should stop and pick a library.
The patterns in lesson 6 — buttons, disclosures, tabs — fit on a screen. The patterns in this lesson don't. Composite widgets are interactive components made of multiple roles working together, where the keyboard contract is intricate and easy to break.
This lesson is a survey, not a tutorial. The goal is for you to recognize each pattern, know what it costs to build correctly, and decide between writing it yourself, picking a library, or — when possible — switching to a simpler design.
What "composite" means
A composite widget is one that owns a group of focusable children, but exposes only one of them to the page's Tab key. Inside the widget, arrow keys, Home, End, type-ahead, and sometimes more let the user navigate. The rest of the page never sees those keystrokes.
That's the same "roving tabindex" idea you saw with tabs in lesson 6. Composite widgets push it further. The keyboard contract for a tree view alone has roughly fifteen keystrokes. A grid has thirty. Each one is a chance to ship a bug.
Heuristic: if your widget needs a state diagram to explain, or if "what does Home do?" has a non-obvious answer, you're building a composite widget. Pause. Read the relevant APG pattern (lesson 8) before writing code.
Listbox
A listbox is a list of selectable options. Picture the dropdown part of a <select> — that's a native listbox. The ARIA recipe:
<ul role="listbox" aria-label="Choose color" tabindex="0" aria-activedescendant="opt-blue"> <li role="option" id="opt-red" aria-selected="false">Red</li> <li role="option" id="opt-blue" aria-selected="true">Blue</li> <li role="option" id="opt-green" aria-selected="false">Green</li> </ul>
Two things are unusual:
- The list itself is focusable, not the individual options. The user tabs to the list once.
aria-activedescendantpoints to the currently active option's ID. As the user presses arrow keys, you update that attribute — the screen reader announces the new option, even though browser focus stays on the list.
Listboxes support arrow up/down (move active option), Home / End (jump to first / last), type-ahead (typing letters jumps to matching options), and Space or Enter (select). For multi-select listboxes, Shift+click and Ctrl+click extend the selection — which means many more key handlers.
The native <select> element gives you all of this for free. Use it. Reach for role="listbox" only when you need rich options the native element can't render (icons, multi-line text, custom layout).
Combobox
A combobox is a textbox plus a popup of suggestions. Search-as-you-type, autocomplete, country pickers. It's the most-shipped composite widget and the one most teams get wrong.
The shape, simplified:
<label for="city">City</label>
<input id="city"
role="combobox"
aria-expanded="false"
aria-controls="city-listbox"
aria-autocomplete="list">
<ul id="city-listbox" role="listbox" hidden>
<li role="option" id="city-1">Berlin</li>
<li role="option" id="city-2">Bristol</li>
</ul>When the listbox opens, set aria-expanded="true" on the input. When the user arrows down into the suggestions, update aria-activedescendant on the input — focus stays in the input so they can keep typing. Selecting an option puts its text in the input, closes the popup, and resets aria-expanded to false.
Combobox keyboard expectations:
- Down arrow opens the popup (or moves to the next option if already open).
- Up arrow moves to the previous option.
- Escape closes the popup.
- Enter selects the active option.
- Tab moves out, often selecting the active option as a side effect.
There are several subpatterns of combobox depending on whether you want autocomplete to be inline, a list, or both. Each has slightly different ARIA. The combobox spec was rewritten between ARIA 1.0 and 1.2 — old code on the web frequently uses the old shape, which doesn't work in modern screen readers. If you copy a combobox snippet, check it's the 1.2 version.
Tree view
A tree view is a hierarchical list — a file explorer, an org chart, nested categories. Roles: tree on the container, treeitem on each node, plus aria-expanded to mark expandable items and aria-level, aria-posinset, aria-setsize to expose the structure.
Keyboard contract (just the basics): Up / Down move between visible items, Right opens (or moves to first child), Left closes (or moves to parent), Home / End jump to first / last. Add type-ahead for usability.
Tree views are deceptively hard because the keyboard semantics depend on whether the current item is expanded or collapsed, whether it has children, and whether you support multi-select. Also: if your tree has tens of thousands of items, you'll want virtualization, which complicates aria-setsize further (you'll be reporting "item 5 of 18,000," but only 30 items are in the DOM).
Grid
A grid is a two-dimensional table of cells where the user can navigate cell-by-cell with arrow keys — spreadsheets, complex data tables with selection. Not every table is a grid; most are static <table> elements which is the right tool. Grids are the interactive ones.
Roles: grid on the container, row on rows, gridcell (or columnheader / rowheader) on cells. Keyboard: arrow keys move between cells, Home / End jump within a row, Ctrl+Home / Ctrl+End jump to the first / last cell, Tab moves between focusable widgets within the active cell (yes — cells can hold inputs).
If you don't need any of that interaction, use a plain <table> with <th scope="col">. That gives screen readers everything they need without the keyboard maze.
Library or hand-roll?
For composite widgets, this is the honest decision tree:
- Can the design use a native element instead?
<select>,<details>, a static<table>, plain anchors. If yes, do that. - Is the design close enough to a library widget? Reach for a well-tested headless library (Radix, React Aria, Headless UI, Reach UI) where the ARIA wiring and keyboard contract are already solved. The library handles the spec; you handle the styling.
- Truly bespoke? Read the relevant APG pattern (lesson 8), test with real screen readers (NVDA, VoiceOver, JAWS), and budget time for the tail of bug fixes. Grids and combos in particular tend to surface a new edge case every quarter.
"We'll just sprinkle some ARIA on it" is the famous last words of a custom select. The keyboard contract is the work, not the attributes.