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

When not to use it

Keyboard interactions

Role combobox, verified at WCAG 2.2-AA.

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'}]} />

See the full Combobox reference →