Frontend Handbook
Written

Design an autocomplete

The method applied to a typeahead: requirements and architecture, the request lifecycle (debounce, cancel, and why the latest query must win), and the accessible combobox keyboard pattern, with live demos.

"Design an autocomplete" sounds small and isn't. Suggesting results as the user types hides the decisions senior interviews probe: debouncing, cancelling stale requests, dropping out-of-order responses, caching, and the full accessible combobox keyboard pattern. This page runs the prompt through the design method and links the built debounced-search practice as the lifecycle implementation; the demos here make the race condition and the keyboard pattern concrete.

01

Requirements and architecture

Scope it first, then sketch the small set of pieces: an input, a results list, and a query engine with a cache in front of a suggestion source.

  • Functional: show suggestions while typing; choose a suggestion by mouse or keyboard; highlight the matched text; handle no-results and errors.
  • Non-functional: suggestions within ~100ms of a pause, no jank per keystroke, the latest keystroke's results always win, and a full keyboard/screen-reader experience.
  • Scope cuts: fix the corpus size and where it lives, pick prefix or fuzzy matching, and decide how many suggestions — each changes the design.
  • Architecture: keep a query engine between the input and the network so debounce, caching and the latest-request guard live in one place. An alternative pushes matching to an edge search index close to users.

Try it: the autocomplete's architecture

Autocomplete architectureA combobox input and a results listbox sit over a query engine that debounces input, checks a query-keyed cache, and otherwise calls a suggestion source; a latest-request guard drops out-of-order responses.BrowserCombobox inputrole=comboboxResults listboxrole=listboxQuery enginedebounce + guardCacheby normalised queryServer / edgeSuggestion sourcesearch index
One example. Solid arrows: a request path (input → engine → cache or source). Dashed: the response, which the latest-request guard may drop. A larger system would put the suggestion source behind an edge search index close to users.
  • Do: Put a query engine between input and network; decide corpus location and match type up front.

    Avoid: Calling the network straight from the input's change handler with no debounce, cache or guard.

Read more:The design method(opens in a new tab)Debounced Search practice(opens in a new tab)

02

The request lifecycle

The signature autocomplete bug is a race: you type, several requests are in flight, and a slow earlier one resolves after a fast later one — leaving the box showing results for a query the user has already moved past. Three habits fix the lifecycle.

  • Debounce so a burst of keystrokes issues one request, not one per character.
  • Drop out-of-order responses. Tag each request and let only the latest one update the UI (cancel the rest where you can). A response for an old query must never win.
  • Cache by the normalised query. A repeat query (including backspacing to a previous one) is served instantly with no request.
  • Run the demo with the guard off to see stale results win, then on; toggle debounce to watch the request count change.

Try it: debounce, cancel, and the out-of-order race

Showing results for: —

  • no results

Request log0 request(s)

  1. nothing yet

Simulated backend over a local word list. With the guard off, the burst ends on an earlier query's results because it resolves last; with it on, only the latest query wins. Debounce cuts the request count; a repeat query is served from cache with no request.

  • Do: Debounce, tag requests and apply only the latest, and cache by the normalised query.

    Avoid: Trusting response arrival order, or relying on cancellation alone for correctness (treat it as an optimisation).

Read more:Debounced Search + API Cache(opens in a new tab)MDN: AbortController(opens in a new tab)

03

Deep dives: accessibility and ranking

Autocomplete is a keyboard-first control a screen reader must narrate. Getting the combobox pattern right is a senior expectation; ranking and highlighting are the finishing touches.

  • The combobox pattern. A role="combobox" input exposes aria-expanded, aria-controls and aria-activedescendant; the popup is a role="listbox" of role="option" items. Arrow keys move the active option (wrapping), Enter selects, Escape closes — focus stays in the input throughout.
  • Highlight the match so users see why each suggestion appeared, and keep the list short.
  • Ranking decides order: prefix matches before substring, shorter before longer, popularity if you have it. Prefix matching is cheap and predictable; fuzzy matching finds more but can surprise — and heavier ranking belongs on the server. Name the trade-off rather than claiming one is best.
  • The request lifecycle above is implemented in the debounced-search practice; the combobox below is the accessible shell around it.

Try it: an accessible combobox

Type to see suggestions; Arrow keys move, Enter selects, Escape closes.

  • Do: Implement the full combobox ARIA pattern and keyboard model; highlight matches; rank prefix-first.

    Avoid: A div soup with no roles, swallowing Escape/Enter, or moving DOM focus into the option list.

Read more:WAI-ARIA: combobox pattern(opens in a new tab)MDN: ARIA combobox role(opens in a new tab)

Next case studies: a chat application and a design system — each applying the same method to a different shape of problem.