Combobox & autocomplete

A combobox is a text input paired with a filtered list of suggestions: the user types, the list narrows, and they can pick an entry with the keyboard or pointer without losing the ability to type freely. It is one of the more involved widgets to get right, because focus has to stay in the input while a separate listbox is navigated — so reach for a plain <select>, or an <input list> tied to a <datalist>, whenever they cover the need. Build this richer combobox only when you genuinely need filtering, custom option rendering, or behaviour the native controls can’t give you.

The widget below follows the WAI-ARIA Authoring Practices editable-combobox pattern with list autocomplete. The page is a reference implementation: the markup is authored statically, and combobox.js enhances it — with scripting off, the field is still a labelled text input.

Live demo

Focus the field or press Down Arrow to open the list, type to filter, use the arrow keys to move the highlight, and press Enter to choose. Notice that your typing cursor never leaves the input — the highlighted option is tracked with aria-activedescendant, not by moving focus.

When to use

Prefer the native control first

  • If the user just picks one value from a fixed, short list, use a <select> — it is keyboard- and screen-reader-complete for free.
  • If you want type-ahead suggestions over a list but don’t need custom rendering or filtering control, try <input list="…"> with a <datalist>.
  • Build this ARIA combobox when you need filtering as the user types, custom option markup, async results, or behaviour the native widgets can’t express — and budget for the keyboard model below.
  • Whatever you choose, the field must keep a real, visible <label> tied to it by for / id.

Markup

Author the input and listbox statically with their ARIA wiring already in place, then let combobox.js manage state. The script adds option ids if missing, but providing them keeps the markup self-describing.

combobox.html
<div class="ewa-combobox" data-combobox>
  <label class="ewa-field__label" id="cb-label" for="cb-input">Choose a WCAG principle</label>
  <input class="ewa-input ewa-combobox__input" id="cb-input" type="text"
         role="combobox" aria-expanded="false" aria-controls="cb-listbox"
         aria-autocomplete="list" autocomplete="off">
  <ul class="ewa-combobox__listbox" id="cb-listbox" role="listbox"
      aria-labelledby="cb-label" hidden>
    <li class="ewa-combobox__option" id="cb-opt-0" role="option">Perceivable</li>
    <li class="ewa-combobox__option" id="cb-opt-1" role="option">Operable</li>
    <li class="ewa-combobox__option" id="cb-opt-2" role="option">Understandable</li>
    <li class="ewa-combobox__option" id="cb-opt-3" role="option">Robust</li>
    <!-- …more options… -->
  </ul>
</div>

Keyboard interactions

Focus stays on the input throughout. The keys below act on the listbox while the caret stays put, so a keyboard or screen-reader user drives everything from the field.

Keyboard interactions (focus remains on the input)
Key When Result
Down Arrow List closed Opens the list and moves the highlight to the first option.
Down Arrow List open Moves the highlight to the next visible option; wraps from last to first.
Up Arrow List open Moves the highlight to the previous visible option; wraps from first to last.
Enter An option is highlighted Selects it: its text fills the input, the list closes, focus stays on the input.
Esc List open Closes the list and clears the highlight without changing the typed value.
Typing / editing Any time Filters options to those containing the text (case-insensitive); a “No matches” row shows when none match.
Home / End List open (optional) Jumps the highlight to the first / last visible option.

ARIA roles, states, and properties

The input owns the combobox role and the open/active state; the popup is a listbox of options. The active option is referenced from the input, never focused in the DOM.

ARIA attributes and where they live
Attribute On Purpose
role="combobox" Input Identifies the text field as a combobox that controls a popup.
role="listbox" Popup <ul> Marks the suggestion popup as a list of selectable options.
role="option" Each <li> Marks each suggestion as a selectable option within the listbox.
aria-expanded Input true while the listbox is shown, false when closed.
aria-controls Input Points at the listbox’s id so AT knows what the input drives.
aria-activedescendant Input Holds the id of the highlighted option; always a visible option, or absent.
aria-autocomplete="list" Input Tells AT that typing filters a list of suggestions (not inline text completion).
aria-selected="true" Active option Marks the single highlighted option; removed from all others.
aria-labelledby Listbox Names the popup from the same visible label as the field.

Common mistakes

Pitfalls to avoid

  • Moving DOM focus into the options. Focus must stay on the input; track the highlight with aria-activedescendant, or the typing cursor and the screen-reader announcement fall out of sync.
  • Pointing aria-activedescendant at a hidden option. After filtering, the referenced id must still be a visible, selectable option — otherwise remove the attribute entirely.
  • Making the “No matches” row selectable. It should be aria-disabled and never become the active descendant.
  • Closing the list on the option click’s blur. Commit the choice on mousedown (with preventDefault) so the input’s blur doesn’t close the popup before the click lands.
  • Relying on colour alone for the highlight. Pair the active background with a second cue and remap to Highlight / HighlightText under forced colours.
  • Dropping the visible label. A placeholder is not a label; keep a real <label for> on the input.