React - Autocomplete / Typeahead
Overview
Estimated time: 20–30 minutes
Implement a typeahead input with debounced filtering, arrow key navigation, and aria attributes.
Try it: Typeahead
View source
function useDebounced(value, ms){
const [v, setV] = React.useState(value);
React.useEffect(() => { const t = setTimeout(() => setV(value), ms); return () => clearTimeout(t); }, [value, ms]);
return v;
}
function Typeahead(){
const data = ['Apple','Apricot','Banana','Blackberry','Blueberry','Cherry','Grape','Grapefruit','Lemon','Lime','Mango','Orange','Peach','Pear','Pineapple','Plum','Raspberry','Strawberry','Watermelon'];
const [q, setQ] = React.useState('');
const dq = useDebounced(q, 120);
const [active, setActive] = React.useState(-1);
const results = dq? data.filter(x => x.toLowerCase().includes(dq.toLowerCase())) : [];
const listId = 'ta-list';
return (
<div role="combobox" aria-expanded={results.length>0} aria-owns={listId} aria-haspopup="listbox">
<input aria-autocomplete="list" aria-controls={listId} value={q} onChange={e => { setQ(e.target.value); setActive(-1);} } onKeyDown={e => {
if (e.key==='ArrowDown') { setActive(a => Math.min(results.length-1, a+1)); e.preventDefault(); }
if (e.key==='ArrowUp') { setActive(a => Math.max(0, a-1)); e.preventDefault(); }
if (e.key==='Enter' && active>=0) { setQ(results[active]); setActive(-1); }
}} placeholder="Search fruits" />
{results.length > 0 && (
<ul id={listId} role="listbox" style={{border:'1px solid var(--border)', marginTop:4, borderRadius:6, padding:4}}>
{results.map((x,i) => (
<li key={x} role="option" aria-selected={i===active} onMouseEnter={() => setActive(i)} onMouseDown={(e) => { e.preventDefault(); setQ(x); setActive(-1);} } style={{padding:'4px 6px', borderRadius:4, background: i===active? 'var(--accent-subtle)':'transparent'}}>
{x}
</li>
))}
</ul>
)}
</div>
);
}
ReactDOM.createRoot(document.getElementById('try-typeahead')).render(<Typeahead />);
Syntax primer
- Debounce input to avoid filtering on every keystroke.
- Use WAI-ARIA combobox pattern roles and attributes.
Common pitfalls
- Click selection blur—use onMouseDown to select without losing focus first.
Exercises
- Load suggestions from a network API and show a loading state.
Checks for Understanding
- Which WAI-ARIA roles/attributes are used for an autocomplete combobox pattern?
- Why debounce the input value before filtering?
- Why is onMouseDown used for selecting an option instead of onClick?
Answers
- Use role="combobox" with aria-haspopup="listbox", aria-owns/aria-controls pointing to the listbox, and list items with role="option" and aria-selected on the active item.
- To avoid running filter logic on every keystroke and to reduce flicker/overwork when users type quickly.
- onMouseDown fires before the input loses focus, allowing selection without the list closing due to blur.