QuestionsAccessibility

Accessible combobox / autocomplete (APG)

ARIA WidgetsHardAccessibility

Design an accessible autocomplete. What roles, ARIA state, and keyboard model does the combobox pattern require?

What it tests

The combobox/listbox wiring: aria-activedescendant vs focus, expanded state, and the full key map.

Approach & answer

A combobox is an input paired with a popup (usually a listbox of suggestions) — one of the hardest widgets to get right. Structure per the APG: a text <input> with role="combobox" (native input already, but the role/attrs make the popup relationship explicit), aria-expanded reflecting whether the popup is open, aria-controls pointing at the listbox id, and aria-autocomplete="list" (or 'both' when it also inline-completes). The popup is role="listbox" containing role="option" items, each with a unique id and aria-selected on the active one. The key decision is HOW focus works: keep DOM focus on the INPUT (so the user can keep typing) and track the highlighted option with aria-activedescendant on the input, set to the active option's id — the screen reader announces that option while real focus never leaves the input. (The alternative, moving real focus into the list, breaks typing.) Keyboard model: Down/Up arrows move the active option (opening the popup if closed) and update aria-activedescendant; Enter selects the active option and closes; Escape closes the popup (and on a second press may clear the input); Home/End jump within the list; typing filters. On selection, put the value in the input, collapse the popup (aria-expanded="false"), and clear aria-activedescendant. Announce result counts via a polite live region ('5 results available') so screen-reader users know suggestions appeared. Because this is so error-prone, most teams should use a vetted library or the emerging native options — but you must be able to reason about the roles, the activedescendant model, and the key map.

Use this technique when

Building autocomplete/typeahead/select-with-search; explaining activedescendant vs moving focus.

Code

<label for="city">City</label>
<input id="city" role="combobox"
       aria-expanded="true"
       aria-controls="city-list"
       aria-autocomplete="list"
       aria-activedescendant="city-opt-2">   <!-- focus stays here; this id = highlighted option -->

<ul id="city-list" role="listbox">
  <li id="city-opt-1" role="option">London</li>
  <li id="city-opt-2" role="option" aria-selected="true">Lisbon</li>
  <li id="city-opt-3" role="option">Lima</li>
</ul>

<div aria-live="polite" class="sr-only">3 results available</div>
<!-- Keys: ↓/↑ move activedescendant, Enter selects, Esc closes, typing filters -->

References