AutoComplete Atom
A Select you can type into. Filtering, highlight, and keyboard behaviour are Base UI's.
It holds the whole list by default and matches it in the browser. Past the point where
shipping every row is the wrong trade, hand it loadItems instead and it asks for them
one query at a time.
Usage
Type to narrow the list.
Selected: id
<Field label="Country" hint="Type to narrow the list."> <AutoComplete items={countries} value={value} onValueChange={setValue} placeholder="Search countries…" /></Field>Do
Reach for a AutoComplete past about ten options, where typing beats scanning.
Don't
Use it for four options — a Select shows them all in one click, and a segmented RadioGroup shows them without any.
Select's page carries the table for choosing between the three, with the usability research the numbers come from. The short version: a dropdown is worth having between roughly five and ten options, and past that the advice is to replace it with a control you can type into — which is this one.
Asking somewhere else
loadItems puts the AutoComplete in async mode. It is given the query and an AbortSignal,
and it returns the rows to offer.
<AutoComplete placeholder="Search places" loadItems={async (query, signal) => { const res = await fetch(`/api/places?q=${encodeURIComponent(query)}`, { signal }); return (await res.json()).map((row) => ({ value: row.id, label: row.name })); }}/>Three things it does so every caller does not have to:
- Asks once for a burst of typing.
loadDelayis how long typing has to stop first. - Ignores a stale reply. A slow
alanding after a fastabccannot overwrite the newer list, and the superseded request's signal is aborted. - Stops matching in the browser. Whatever answered the query already decided what matches; matching the reply again drops rows whose label does not happen to spell the query — a synonym, an id, an alias.
The popup says which of the three states it is in: searching, failed, or nothing matched. They are different answers, and showing the last one during the wait is a wrong answer that arrives before the right one.
Do
Give loadItems when the list is too large to ship, or lives behind a search endpoint.
Don't
Give both items and loadItems — loadItems wins, and the static list is then a lie.
Props
| Prop | Type | Default | Notes |
|---|---|---|---|
| items | ComboboxOption[] | — | value, label, and an optional description line. Omit when loadItems is given. |
| loadItems | (query, signal) => Promise<ComboboxOption[]> | — | Asks somewhere else for the rows. Switches off browser-side matching. |
| loadDelay | number | 250 | How long typing has to stop before loadItems is asked. |
| loadingMessage / errorMessage | ReactNode | — | What the popup says while a search is out, and when it failed. |
| value / defaultValue | string | null | — | Controlled or uncontrolled. null is "nothing chosen". |
| onValueChange | (value: string | null) => void | — | Clearing reports null rather than an empty string. |
| emptyMessage | ReactNode | 'Nothing matched' | Shown when the query matches nothing — never an empty popup. |
| size / disabled / invalid | — | — | As every other control; inside a Field, invalid is inherited. |