You can type anything. The list only suggests.
import { Search } from "lucide-react"
import {When to use it#
Use an autocomplete when people might know what they want to type and the list is only there to save keystrokes: a search box, a city field, a product lookup. The value is the text in the input, so it can be something that is not in the list at all.
If the answer has to be one of the options, such as a customer id or a status, use a Combobox instead. If the list is short and fixed, a Select is simpler.
Import#
import {
Autocomplete,
AutocompleteInputGroup,
AutocompleteInput,
AutocompleteClear,
AutocompleteTrigger,
AutocompleteContent,
AutocompleteList,
AutocompleteItem,
AutocompleteEmpty,
} from "@hel/ui/autocomplete"Anatomy#
<Field>
<FieldLabel>Product</FieldLabel>
<Autocomplete items={products} itemToStringValue={(p) => p.name}>
<AutocompleteInputGroup icon={Search}>
<AutocompleteInput placeholder="Search by name" />
<AutocompleteClear />
</AutocompleteInputGroup>
<AutocompleteContent>
<AutocompleteEmpty />
<AutocompleteList>
{(p) => <AutocompleteItem key={p.id} value={p} label={p.name} />}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
</Field>Objects need itemToStringValue so the input knows what text to show when one is picked. The list is a render function, which lets the component filter and virtualise it for you. Put the FieldLabel around the input, or give it an aria-label, because the input needs a name of its own.
Examples#
Controlled text#
Pass value and onValueChange when the rest of the form needs the text. AutocompleteTrigger opens the list without typing, and openOnInputClick opens it on a plain click too.
No city yet.
import * as React from "react"
import {Grouped suggestions#
Give the items as { value, items } groups. Render each group with AutocompleteGroup and its rows with AutocompleteCollection. Groups with no matches disappear as the person types.
import { Search } from "lucide-react"
import {Search on a server#
When the matches come from an API, turn the built-in filter off with filter={null}, fetch inside onValueChange and pass the answer as items. AutocompleteStatus is read out by screen readers, so say what is going on there. The demo ignores answers from older keystrokes, so a slow reply can't overwrite a newer one.
Results arrive from a pretend server after 600ms.
import * as React from "react"
import { Search, Users } from "lucide-react"
A command box#
Give the items an onClick and the list runs actions instead of filling the input. autoHighlight marks the first match so Enter runs it, and trailing is a good place for a shortcut.
Pick one, or press Enter on the first match.
import * as React from "react"
import {
FilePlus2,Other options#
- Inline completion.
mode="both"writes the rest of the highlighted suggestion into the input as you arrow through the list."inline"does that without filtering, and"none"does neither. - Limit the results.
limit={5}shows only the first five matches, which keeps long lists fast.useAutocompleteFiltergives you the same locale-aware matching if you need to count the rest. - Hide an empty popup.
hideWhenEmptyonAutocompleteContentcloses the popup when nothing matches, instead of showing the empty message. - Submit on pick.
submitOnItemClicksubmits the form when a suggestion is chosen, which suits search boxes.
Labels and languages#
The clear button, the trigger and the empty message have English text by default. Add your own under messages in your Hel config, keyed by language, or reword them for a single field with the messages prop.
export const helConfig = defineConfig({
messages: {
de: {
autocomplete: {
clear: "Löschen",
showSuggestions: "Vorschläge anzeigen",
noResults: "Keine Ergebnisse.",
},
},
},
})Keyboard#
| Key | Action |
|---|---|
↓ ↑ | Moves through the suggestions. |
Enter | Picks the highlighted suggestion. |
Esc | Closes the list. A second press clears the text. |
Home End | Move the caret in the text, like any input. |
Props#
Autocomplete#
| Prop | Type | Default |
|---|---|---|
items | Item[] or { value, items }[] | none |
value | string | none |
defaultValue | string | "" |
onValueChange | (value: string, details) => void | none |
mode | "list" | "both" | "inline" | "none" | "list" |
itemToStringValue | (item) => string | none |
filter | (item, query) => boolean or null | built in |
limit | number | none |
autoHighlight | boolean | "always" | false |
openOnInputClick | boolean | false |
submitOnItemClick | boolean | false |
open / onOpenChange | boolean / (open: boolean) => void | none |
locale | string | the app's locale |
messages | Partial<AutocompleteMessages> | none |
Parts#
| Part | What it takes |
|---|---|
AutocompleteInputGroup | icon for a decorative leading icon. It draws the box. |
AutocompleteInput | The text field. |
AutocompleteClear | A button that empties the text. It only shows while there is text. |
AutocompleteTrigger | A button that opens the list. |
AutocompleteContent | hideWhenEmpty, align, sideOffset, anchor. The popup is at least as wide as the box. |
AutocompleteList | A function child that renders each item. |
AutocompleteItem | value, plus label, description, icon, trailing and onClick. Children replace label. |
AutocompleteGroup, AutocompleteGroupLabel, AutocompleteCollection | For grouped items. |
AutocompleteEmpty | Shown when nothing matches. Reads "No results found." by default. |
AutocompleteStatus | A polite status line for async lists. |