The hardest small component
An autocomplete looks like an input and a list. Underneath, it's one of the hardest widgets on the web: requests that come back out of order, results worth caching, a list that must open and close at the right moments, and keyboard and screen-reader support that most implementations get wrong. That makes it the perfect capstone — it needs the patterns from this whole course at once.
You'll build a combobox following the WAI-ARIA Authoring Practices: a reducer for the UI state (lesson 6), a cache for results (lesson 7), an effect that can't suffer a race condition, and the ARIA attributes that let everyone use it. Then you'll see how teams test a component like this the way users use it.
The combobox pattern
Focus stays in the input the whole time. Arrow keys move a *virtual* highlight through the options, and the input announces which option is active with aria-activedescendant, pointing at that option's id. Screen readers read the active option as if it had focus, while the user keeps typing.
<input
role="combobox"
aria-expanded={showList}
aria-controls="results"
aria-autocomplete="list"
aria-activedescendant={active >= 0 ? "opt-" + active : undefined}
/>
<ul id="results" role="listbox">
<li id="opt-0" role="option" aria-selected={active === 0}>Momo</li>
</ul>- ArrowDown / ArrowUp move the highlight and wrap at the ends; call preventDefault() so the caret doesn't jump.
- Enter picks the highlighted option, fills the input and closes the list.
- Escape closes the list and keeps what was typed.
- Pick with onMouseDown + preventDefault() so clicking an option doesn't blur the input first.
- Give the input a visible <label> — placeholder text isn't a label.