Implement an Accessible Autocomplete Search Component
AmazonFrontend Engineer · Technical ScreenLast updated
Design and implement an accessible, race-condition-resilient autocomplete search component in React or modern JavaScript, focusing on debounced queries, ARIA combobox semantics, and keyboard navigation.
Problem Description
Build an accessible, production-ready autocomplete (combobox) search component. The component accepts a user query, debounces the input, fetches matching suggestions from an asynchronous endpoint, and renders a listbox of results.
Because network latency varies, responses may arrive out of order (for example, a slower response for query "ap" might resolve after a faster response for "appl"). Your component must guarantee that suggestions displayed always reflect the current input query and never flash stale data.
Requirements
- Race Condition & Lifecycle Handling: Prevent stale responses from overwriting newer queries. Implement request cancellation (e.g., via
AbortController) or an incremental request sequence counter. - Debouncing: Delay triggering fetch calls by a configurable
debounceMs(default 300ms). - Accessibility (ARIA Combobox Pattern):
- Input element must have
role="combobox",aria-autocomplete="list",aria-expanded(true/false), andaria-controlspointing to the listbox ID. - Suggestions container must have
role="listbox"and each item must haverole="option". - Visual selection must be tracked via
aria-activedescendantreferencing the focused option's ID, keeping the actual DOM focus within the input.
- Input element must have
- Keyboard Navigation:
ArrowDown/ArrowUp: Move visual focus through suggestions (wrapping or stopping at bounds).Enter: Select currently highlighted suggestion and close dropdown.Escape: Close suggestions dropdown without clearing input.
- UX & State:
- Support loading and empty ("No results found") states.
- Close suggestions when the user clicks outside or blurs the component.
Component Signature (TypeScript / React)
interface AutocompleteProps {
fetchSuggestions: (query: string, signal?: AbortSignal) => Promise;
renderOption: (item: T) => React.ReactNode;
getOptionKey: (item: T) => string;
onSelect: (item: T) => void;
debounceMs?: number;
placeholder?: string;
}
function Autocomplete(props: AutocompleteProps): JSX.Element;
Example Flow
- User types
"b"att = 0ms. - User types
"ba"att = 100ms. - The debounce timer expires at
t = 400ms, sending a network request for"ba". - If user presses
ArrowDown, the first suggestion becomes visually active andaria-activedescendantis set to its ID. - Pressing
EnterexecutesonSelectwith the active item and collapses the listbox.
Edge Cases
- Empty input: Fetch should not trigger; close the suggestions list immediately.
- Network failure: Catch rejected promises cleanly and display an accessible error state.
- Unmounting: Cancel active requests and clear pending timeouts to prevent memory leaks.
Hints
Hint 1
Use AbortController inside a custom hook or an effect cleanup function to abort pending fetch requests whenever the query changes or unmounts.
Hint 2
For the ARIA combobox pattern, manage active item indices in state and dynamically assign aria-activedescendant to the focused option's DOM id rather than moving DOM focus out of the input.
Hint 3
Ensure cleanup of window event listeners and debounce timeout timers on every render cycle to avoid duplicate handlers and memory leaks.
Follow-up questions
- How would you integrate client-side caching (e.g., LRU cache) to avoid refetching identical queries while ensuring cached results do not display stale data?
- If the query returns thousands of items, how would you incorporate list virtualization while maintaining screen-reader announcements and keyboard navigation?
- How would you test this component using React Testing Library to verify that out-of-order API responses are correctly ignored?
Solution walkthrough is for members
Get the full walkthrough with reference code and complexity analysis, plus Kitty Coach at 300 messages a day. Hints, the code runner and community answers stay free.