AccessScanRun a free scan

Guide

Accessible Search Autocomplete: The ARIA Combobox Pattern Done Right

Search-as-you-type is one of the most common widgets on the web, and one of the most commonly broken for assistive technology. An accessible search autocomplete needs more than a styled dropdown: it needs the correct ARIA combobox roles, predictable keyboard handling, a live region that announces how many results appeared, and a label the input actually exposes.

This guide walks through the WAI-ARIA combobox pattern as it applies to a search field, with concrete markup and the keyboard and screen-reader behavior developers most often get wrong. Get these four things right and your autocomplete will work for keyboard and screen-reader users without surprises.

The ARIA combobox pattern for search

A search autocomplete is a combobox: a text input paired with a popup that presents suggestions. Since ARIA 1.1 the pattern moved the supporting roles off the wrapper and onto the input itself. The structure that works across modern screen readers looks like this:

  • The text input has role="combobox" (a plain text input exposes this implicitly, but setting it is harmless and explicit), aria-expanded reflecting whether the listbox is showing, and aria-controls pointing to the listbox id.
  • The popup is a separate element with role="listbox" and a stable id referenced by aria-controls.
  • Each suggestion is a role="option" element with a unique id and aria-selected on the currently highlighted one.
  • The input uses aria-autocomplete="list" (or "both" if you also fill the input text as the user arrows through results).

Critically, the input keeps DOM focus the entire time. The user never tabs into the listbox. Instead, you set aria-activedescendant on the input to the id of the highlighted option. This is the single most misunderstood detail of the pattern: focus stays put, and a virtual cursor moves through options via aria-activedescendant.

A minimal skeleton:

  • <label for="q">Search</label>
  • <input id="q" role="combobox" aria-expanded="false" aria-controls="results" aria-autocomplete="list" aria-activedescendant="">
  • <ul id="results" role="listbox"> <li id="opt-1" role="option">...</li> </ul>

Do not put role="search" on the combobox itself. Wrap the whole field in a <search> element (or a <div role="search">) as a landmark, but keep combobox semantics on the input.

Keyboard interaction that users expect

Keyboard support is non-negotiable under WCAG 2.1.1 Keyboard (Level A), and getting the conventional keys right is what makes autocomplete feel native. Bind these on the input, not on the options:

  • Down Arrow: open the listbox if closed; otherwise move aria-activedescendant to the next option, wrapping or stopping at the end per your preference.
  • Up Arrow: move to the previous option; from the first option, return focus to the typed text.
  • Enter: select the active option, place its value in the input, close the listbox, and submit or navigate.
  • Escape: first press closes the listbox without changing the input; a second press may clear the field.
  • Home / End: move the text cursor within the input (do not hijack these for the list).
  • Tab: move focus out of the widget. If an option is active, most implementations select it first, then move on.

Two failure modes dominate real audits. First, trapping focus inside the dropdown so Tab cannot escape, which violates 2.1.2 No Keyboard Trap (Level A). Second, swallowing Escape so a keyboard user cannot dismiss suggestions covering the page. Both are easy to catch with a quick keyboard-only pass. For the broader principles, see our guide to keyboard accessibility.

Also mind target size: when suggestions render as tappable rows, give them at least 24 by 24 CSS pixels to satisfy WCAG 2.2's new 2.5.8 Target Size (Minimum, Level AA).

Announcing results to screen readers

When suggestions appear, a sighted user sees them instantly. A screen-reader user hears nothing unless you tell them. This is where most autocompletes silently fail, and it maps directly to WCAG 4.1.3 Status Messages (Level AA): a status that conveys results must be announced without moving focus.

Add a visually hidden live region near the input and write a short count message into it whenever the result set changes:

  • <div aria-live="polite" class="sr-only"></div>
  • On results: set its text to "8 suggestions available." On no results: "No results found."
  • Debounce the update (roughly 200 to 500 ms after typing stops) so it announces the final count, not every intermediate keystroke.

Use aria-live="polite", never "assertive", so the announcement waits for a pause instead of interrupting the letters the user is still typing. Keep the message terse and update only when the count actually changes, otherwise screen readers re-announce noise on every character. The individual options do not need a live region; aria-activedescendant already causes each highlighted option to be read as the user arrows through.

Labels, names, and contrast

An input without an accessible name is announced as just "edit text," leaving screen-reader users to guess. Every search field needs a programmatic label, satisfying 4.1.2 Name, Role, Value (Level A) and 3.3.2 Labels or Instructions (Level A).

  • Prefer a visible <label for> tied to the input id. A magnifying-glass icon is not a label.
  • If the design truly has no visible label, use aria-label="Search" on the input. Placeholder text is not a substitute; it disappears on input and is often too low-contrast.
  • Give the submit button a real name too. An icon-only button needs aria-label="Search" or visually hidden text.
  • Reference the live region's purpose implicitly through its content; it does not need its own label.

Watch contrast on suggestions, especially the highlighted-row state and any matched-text emphasis. Text needs 4.5:1 against its background (3:1 for large text, meaning at least 18pt or 14pt bold), and the focus or hover indicator distinguishing the active option must meet 3:1 as a UI component. A pale grey hover behind grey text is a frequent contrast failure. Verifying labels, roles, and these contrast ratios is exactly the kind of check AccessScan flags; run your page through a scan to catch missing names and weak contrast before they reach users.

These same patterns appear across forms work generally; if you build many input widgets, the accessible forms guide covers labels, errors, and grouping in depth, and meeting WCAG 2.2 Level AA here is part of conformance under the European Accessibility Act.

A quick pre-ship checklist

  • Input has role="combobox", aria-expanded toggles, and aria-controls points to a real listbox id.
  • Options use role="option" with unique ids; the active one is tracked via aria-activedescendant, not roving focus.
  • Arrow keys, Enter, and Escape all behave conventionally and nothing traps Tab.
  • A polite live region announces result counts and the no-results state.
  • The input and submit button each have an accessible name, and highlighted rows meet contrast and target-size minimums.

Check your site against AccessScan

See your issues ranked by impact in seconds — free.

Run a free accessibility scan

FAQ

Should I use a real listbox or a div with ARIA roles?

Either works, because there is no native HTML autocomplete-with-suggestions element. The key is correctness: whatever element holds suggestions must expose role="listbox" with role="option" children, the input must own role="combobox" with aria-expanded and aria-controls, and the highlighted option must be tracked with aria-activedescendant while DOM focus stays on the input.

Why use aria-activedescendant instead of moving focus to each option?

Moving real focus into the list would blur the input, stop keystrokes from reaching it, and make typing-while-browsing impossible. The combobox pattern keeps focus on the input and uses aria-activedescendant to point at the active option, so screen readers announce each suggestion while the user keeps typing or refining.

How do I announce how many results appeared?

Put a visually hidden element with aria-live="polite" near the input and write a short count into it when results change, such as "5 suggestions available" or "No results found." Debounce the update so it reflects the final result set, and use polite rather than assertive so it does not interrupt typing. This satisfies WCAG 4.1.3 Status Messages (Level AA).

Does an HTML datalist make my search accessible?

Sometimes, but support and screen-reader behavior for <input list> plus <datalist> are inconsistent across browsers, and you cannot style it or control announcements. For a production search experience you usually need the explicit ARIA combobox pattern so you control keyboard handling, result announcements, and contrast.

More guides