by @ozten
Foundational patterns for building accessible autocomplete/combobox components with state management, ARIA patterns, keyboard navigation, async suggestions, and framework-agnostic implementation. Use when building autocomplete inputs, command palettes, search inputs, select replacements, or any dropdown with keyboard navigation and suggestions.
Build accessible autocomplete/combobox components using proven patterns from production implementations.
Every library (Downshift, Headless UI, Ariakit, Algolia) converges on remarkably similar state shapes. Understanding this canonical model lets you implement in any framework:
interface FilterInputState {
// Interaction state (the state machine)
status: 'idle' | 'focused' | 'suggesting' | 'loading';
// Input state
inputValue: string;
cursorPosition: number;
// Token state (for multi-select, see webdev-token-input)
tokens: FilterToken[];
activeTokenIndex: number | null;
// Suggestion state
suggestions: Suggestion[];
highlightedIndex: number; // -1 means no highlight
// Async coordination
lastFetchedQuery: string; // Prevents redundant fetches
pendingRequestId: number; // Race condition prevention
}
The highlightedIndex deserves special attention. This is "virtual focus"—the visually highlighted suggestion—while DOM focus stays on the input. The W3C APG mandates this pattern: you communicate the focused option to screen readers via aria-activedescendant rather than actually moving focus. This lets users continue typing while navigating suggestions.
Downshift's key innovation was the state reducer pattern, which lets you intercept and modify any state transition. When you need the menu to stay open after selection (common for multi-select), you override just that transition rather than forking the library:
stateReducer: (state, { type, changes }) => {
if (type === 'ItemClick' || type === 'InputKeyDownEnter') {
return { ...changes, isOpen: true, inputValue: '' };
}
return changes;
}
This pattern allows customization without fighting the library's internal logic.
Required structure with aria-activedescendant for virtual focus:
<input...