ESC

Type to search the knowledge base.

Accessible Combobox Pattern

Build or evaluate comboboxes with APG keyboard behavior, aria-expanded/activedescendant, and filterable listbox pairing.

advanced3 min read
  • accessibility
  • accessible-combobox

A combobox combines a text input with a popup listbox of suggestions. Custom implementations are a frequent a11y failure: mouse-only lists, missing names, focus lost on filter. Prefer native <select> when you don’t need typeahead filtering; use a combobox pattern when you do.

Docs: APG Combobox, ARIA combobox role.

When to use which

Need Control
Fixed short options <select>
Searchable / async options Combobox
Multi-select tags Combobox + listbox (complex) or specialized pattern

Core roles and properties

<label id="country-label" for="country">Country</label>
<input
  id="country"
  role="combobox"
  aria-autocomplete="list"
  aria-expanded="false"
  aria-controls="country-list"
  aria-labelledby="country-label"
/>
<ul id="country-list" role="listbox" hidden>
  <li id="opt-us" role="option">United States</li>
  <li id="opt-ca" role="option">Canada</li>
</ul>
  • aria-expanded reflects popup open state
  • aria-controls points at the listbox
  • Options use role="option"; active option via aria-activedescendant on the input or roving tabindex

Keyboard (APG summary)

Key Behavior
Down Arrow Open (if closed) / move to next option
Up Arrow Previous option
Enter Select active option
Escape Close popup; often clear if empty pattern requires
Printable keys Type to filter / jump

Focus typically stays in the input while aria-activedescendant tracks the highlighted option.

input.setAttribute('aria-activedescendant', optionId);
input.setAttribute('aria-expanded', 'true');
list.hidden = false;

Filtering

Update options in the DOM; ensure empty results are announced:

<div id="country-status" role="status" aria-live="polite">No matches</div>

See aria-live regions.

Don’t reinvent if possible

Use maintained accessible components (Reach, Radix, React Aria, native-ish libraries) and verify with keyboard + SR. Interviewers still expect pattern literacy.

Focus and modal traps

Combobox popup is usually not a modal dialog — Tab should move out of the field per APG variant. Don’t trap Tab inside the list like a dialog unless following a specific APG example that does.

Footguns

  1. Click-only selection.
  2. aria-expanded never updated.
  3. Options not in accessibility tree (display:none mishandled vs hidden).
  4. Losing input value on blur incorrectly.
  5. Missing label.

Interview out-loud answer

“Combobox is input + listbox with aria-expanded/controls and keyboard arrow support. Focus often stays in the input with aria-activedescendant. I prefer native select when filtering isn’t needed, and I test with keyboard and a screen reader.”

Async options

When options load from the network, set aria-busy on the combobox or announce “Loading options” in a live region. When results arrive, update the listbox and announce count: “12 countries available.” Prevent keyboard arrows from no-oping silently during load without feedback.

Extra practice

Write a minimal demo in a scratch file or the playground: one happy path, one failure path, and one boundary input. If you cannot exhibit a bug that the pattern prevents, you do not own the concept yet — re-read the primary docs linked below and tighten the example until the failure is obvious.

Notes from real codebases

Teams that succeed here keep the rules mechanical: lint where possible, CI for the rest, and a short human checklist for what automation cannot see. Document exceptions with an owner name and a removal date so “temporary” escapes do not become permanent architecture.

Further reading

Related guides