Standards · ARIA

Role Widget

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 via aria-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-expanded stuck 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-activedescendant pointing 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>