How to build an accessible Combobox in React
Two APG combobox shapes, chosen by `searchable`. With searchable (the default) the field is an <input role="combobox"> with aria-autocomplete="list" that filters as the user types — the APG editable combobox. With searchable={false} it is a <button role="combobox"> and printable characters type-to-select — the APG select-only combobox. Either way the field itself keeps DOM focus and owns aria-expanded, aria-controls and aria-activedescendant, so assistive technology tracks the active option as the arrows move it; the previous build put aria-activedescendant on a trigger while focus had moved into a separate search input, so nothing was tracked. The listbox holds only role="option" rows (with aria-selected and aria-disabled) and role="group" headings — the loading and no-results messages are siblings of it, as role="listbox" owns no other children. ArrowUp/ArrowDown skip disabled rows and wrap, PageUp/PageDown move by ten, Alt+ArrowDown opens without moving the active option and Alt+ArrowUp closes keeping the value, Enter selects, Escape closes and returns focus to the field, and Tab closes without stealing focus back. Home/End jump the list only in the select-only variant, because in the editable one they belong to the text caret. Outside-pointer and Escape dismissal come from the shared DismissableLayer rather than a hand-rolled document listener. A polite live region reports the result count as the filter narrows. Selection, the active row and the focus ring each carry a non-colour channel under forced-colors, and every control reaches the coarse-pointer target minimum.
When to use a Combobox
- Single-select from a long list where type-to-filter makes finding an option faster
- Form fields where the value is one of many known options (country, assignee, repository)
When not to use it
- Short option lists (≈2–7) where filtering adds no value — use Select or SegmentedControl
- Selecting multiple values — use MultiSelect
- Triggering actions or commands — use Dropdown or CommandMenu
Keyboard interactions
Role combobox, verified at WCAG 2.2-AA.
ArrowDownArrowUpHomeEndPageUpPageDownEnterSpaceEscapeTab
Common mistakes
Avoid: Using Combobox to fire actions like "Delete" or "Export"
Prefer: Use Dropdown for actions; Combobox is for picking a value
Combobox has role="combobox" with a listbox of selectable values and onValueChange semantics — actions belong in a menu
Avoid: Setting searchable={false} on a 200-item list
Prefer: Keep the default searchable so the user can filter
Type-to-select still jumps to a matching option, but without a visible filter a 200-row list is an unusable scroll; searchable={false} is for short lists
Avoid: <Combobox options={remote} onSearchChange={search} />
Prefer: <Combobox options={remote} onSearchChange={search} filter={() => true} />
The built-in matcher still runs over the server’s results and filters them a second time against the same query, hiding rows the server deliberately returned
Example
<Combobox options={[{value:'de',label:'Germany',group:'Europe'},{value:'jp',label:'Japan',group:'Asia'}]} />