combobox
Marks a text input paired with a popup list of values — used for autocomplete, type-ahead search, and custom selects. The native <select> element handles the simple single-select case; reach for role="combobox" only when you need filtering, custom rendering, or remote suggestions.
When to use
Use <select> when the choice is from a short, fixed list and the user does not need to filter. The native element comes with mobile-friendly pickers, full keyboard support, and zero JavaScript.
role="combobox" is for the cases native cannot reach:
- Autocomplete with filtering (city search, user mention).
- Suggestions fetched from a remote API.
- Items rendered with rich content (avatars, secondary text).
- Tag inputs where multiple values can be selected.
ARIA 1.2 changed the combobox pattern significantly. The role goes on the <input>, NOT on the wrapper. The popup is a separate role="listbox" (or tree/grid/dialog) that the combobox owns via aria-controls.
Keyboard + focus contract
Per the APG combobox pattern:
- Focus stays on the input at all times. The listbox is opened via
aria-expanded="true"and the active option is tracked viaaria-activedescendant, NOT by moving focus. - Down arrow opens the popup and moves the active option to the first item.
- Up/Down arrows move the active option within the popup.
- Enter selects the active option and closes the popup.
- Escape closes the popup; on a second Escape, clear the input.
- Typing filters the list (when
aria-autocomplete="list"or"both").
Set aria-autocomplete to "none", "inline", "list", or "both" to describe the suggestion behaviour.
Common failures
- Putting
role="combobox"on the wrapper instead of the<input>(the pre-1.2 pattern). Modern screen readers expect the role on the editable element. - Moving DOM focus into the listbox on arrow-down. The APG mandates
aria-activedescendant; moving focus breaks typing. aria-expandedstuck at"false"even when the popup is visible.- Listbox rendered in the DOM but never linked from
aria-controls. - Filtering the list but leaving
aria-activedescendantpointing at an item that no longer exists.
Example
<label for="city">City</label>
<input
id="city"
type="text"
role="combobox"
aria-controls="cityList"
aria-expanded="false"
aria-autocomplete="list"
aria-activedescendant=""
>
<ul id="cityList" role="listbox" hidden>
<li id="city-1" role="option">London</li>
<li id="city-2" role="option">Lisbon</li>
<li id="city-3" role="option">Ljubljana</li>
</ul>