Autocomplete

A text input that suggests matches while you type. Picking one fills the input, but any text is still allowed.

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. useAutocompleteFilter gives you the same locale-aware matching if you need to count the rest.
  • Hide an empty popup. hideWhenEmpty on AutocompleteContent closes the popup when nothing matches, instead of showing the empty message.
  • Submit on pick. submitOnItemClick submits 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.

hel.config.ts
export const helConfig = defineConfig({
  messages: {
    de: {
      autocomplete: {
        clear: "Löschen",
        showSuggestions: "Vorschläge anzeigen",
        noResults: "Keine Ergebnisse.",
      },
    },
  },
})

Keyboard

KeyAction
↓ ↑Moves through the suggestions.
EnterPicks the highlighted suggestion.
EscCloses the list. A second press clears the text.
Home EndMove the caret in the text, like any input.

Props

Autocomplete

PropTypeDefault
itemsItem[] or { value, items }[]none
valuestringnone
defaultValuestring""
onValueChange(value: string, details) => voidnone
mode"list" | "both" | "inline" | "none""list"
itemToStringValue(item) => stringnone
filter(item, query) => boolean or nullbuilt in
limitnumbernone
autoHighlightboolean | "always"false
openOnInputClickbooleanfalse
submitOnItemClickbooleanfalse
open / onOpenChangeboolean / (open: boolean) => voidnone
localestringthe app's locale
messagesPartial<AutocompleteMessages>none

Parts

PartWhat it takes
AutocompleteInputGroupicon for a decorative leading icon. It draws the box.
AutocompleteInputThe text field.
AutocompleteClearA button that empties the text. It only shows while there is text.
AutocompleteTriggerA button that opens the list.
AutocompleteContenthideWhenEmpty, align, sideOffset, anchor. The popup is at least as wide as the box.
AutocompleteListA function child that renders each item.
AutocompleteItemvalue, plus label, description, icon, trailing and onClick. Children replace label.
AutocompleteGroup, AutocompleteGroupLabel, AutocompleteCollectionFor grouped items.
AutocompleteEmptyShown when nothing matches. Reads "No results found." by default.
AutocompleteStatusA polite status line for async lists.