Skip to content

Pickers and overlays

Autocomplete

A provider that gives an existing text field a filtered collection of completions.

When to use it: Use it when the text remains the person’s query and choosing a result is a separate collection action; use Combobox when a choice must become the field value.

On this page

Example

Lisbon
Reykjavík
Singapore
Tokyo
Warsaw
Autocomplete.tsxtsx
import {
  Autocomplete,
  Label,
  ListBox,
  ListBoxItem,
  SearchField,
  SearchFieldInput,
} from "@comp0/react";

const destinations = ["Lisbon", "Reykjavík", "Singapore", "Tokyo", "Warsaw"];

function contains(textValue: string, inputValue: string) {
  return textValue.toLocaleLowerCase().includes(inputValue.trim().toLocaleLowerCase());
}

export function Example() {
  return (
    <Autocomplete filter={contains}>
      <SearchField as="div" className="flex w-full max-w-xs flex-col gap-1.5">
        <Label className="text-base font-medium text-zinc-900 sm:text-sm dark:text-zinc-100">
          Destination
        </Label>
        <SearchFieldInput
          autoComplete="off"
          className="w-full rounded border border-zinc-950/10 bg-white px-3 py-2.5 text-base text-zinc-950 outline-teal-600 focus-visible:outline-2 sm:py-2 sm:text-sm dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-50 dark:outline-teal-400"
          name="destination"
          placeholder="Search cities"
        />
        <ListBox
          aria-label="Destinations"
          className="max-h-60 overflow-y-auto rounded border border-zinc-950/10 bg-white p-1 dark:border-white/10 dark:bg-zinc-900"
        >
          {destinations.map((destination) => (
            <ListBoxItem
              key={destination}
              className="cursor-pointer rounded px-3 py-2.5 text-base text-zinc-800 data-active:bg-teal-100 data-active:text-teal-950 data-selected:bg-teal-100 data-selected:text-teal-950 sm:py-2 sm:text-sm dark:text-zinc-100 dark:data-active:bg-teal-950 dark:data-active:text-teal-50 dark:data-selected:bg-teal-950 dark:data-selected:text-teal-50"
              value={destination.toLocaleLowerCase()}
            >
              {destination}
            </ListBoxItem>
          ))}
        </ListBox>
      </SearchField>
    </Autocomplete>
  );
}

Searchable menu

Filter command actions while Menu keeps its own activation behavior.

autocomplete.menu.tsxtsx
import { useState } from "react";
import {
  Autocomplete,
  Label,
  Menu,
  MenuItem,
  MenuList,
  MenuPopover,
  MenuTrigger,
  SearchField,
  SearchFieldInput,
} from "@comp0/react";

const commands = ["Archive conversation", "Assign to me", "Mark as unread", "Move to inbox"];

function contains(textValue: string, inputValue: string) {
  return textValue.toLocaleLowerCase().includes(inputValue.trim().toLocaleLowerCase());
}

export function Example() {
  const [open, setOpen] = useState(false);
  const [lastCommand, setLastCommand] = useState("");

  return (
    <Autocomplete disableAutoFocusFirst filter={contains}>
      <div className="flex w-full max-w-xs flex-col items-start gap-1.5">
        <Menu open={open} onToggle={setOpen}>
          <MenuTrigger
            aria-controls="command-search"
            aria-haspopup="dialog"
            className="rounded border border-zinc-950/10 px-3 py-2 text-sm text-zinc-800 dark:border-white/10 dark:text-zinc-100"
          >
            Commands
          </MenuTrigger>
          <MenuPopover
            id="command-search"
            role="dialog"
            aria-label="Command search"
            className="w-64 rounded border-0 bg-white p-2 shadow-lg ring-1 ring-zinc-950/10 dark:bg-zinc-900 dark:shadow-none dark:ring-white/10"
            offset={4}
            placement="bottom start"
          >
            <SearchField as="div" className="flex flex-col gap-1.5 pb-2">
              <Label className="text-base font-medium text-zinc-900 sm:text-sm dark:text-zinc-100">
                Find a command
              </Label>
              <SearchFieldInput
                autoComplete="off"
                className="w-full rounded border border-zinc-950/10 bg-white px-3 py-2.5 text-base text-zinc-950 outline-teal-600 focus-visible:outline-2 sm:py-2 sm:text-sm dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-50 dark:outline-teal-400"
                placeholder="Type to filter"
              />
            </SearchField>
            <MenuList aria-label="Matching commands">
              {commands.map((command) => (
                <MenuItem
                  key={command}
                  className="w-full rounded px-3 py-2.5 text-left text-base text-zinc-800 select-none hover:bg-zinc-100 data-active:bg-teal-100 data-active:text-teal-950 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800 dark:data-active:bg-teal-950 dark:data-active:text-teal-50"
                  onClick={() => setLastCommand(command)}
                  value={command}
                >
                  {command}
                </MenuItem>
              ))}
            </MenuList>
          </MenuPopover>
        </Menu>
        {lastCommand && (
          <p className="text-base text-zinc-600 sm:text-sm dark:text-zinc-400">Ran {lastCommand}</p>
        )}
      </div>
    </Autocomplete>
  );
}

Mention completion

Complete an @mention inside a multi-line message without replacing the rest of it.

@Aisha
@Diego
@Mina
@Ren
autocomplete.mentions.tsxtsx
import { useState } from "react";
import { Autocomplete, Label, ListBox, ListBoxItem, TextArea, TextField } from "@comp0/react";

const teammates = ["Aisha", "Diego", "Mina", "Ren"];

function contains(textValue: string, inputValue: string) {
  return textValue.toLocaleLowerCase().includes(inputValue.toLocaleLowerCase());
}

export function Example() {
  const [message, setMessage] = useState("Could @");
  const mention = message.match(/(?:^|\s)@(\w*)$/);
  const mentionQuery = mention?.[1] ?? "";

  function insertMention(name: string) {
    setMessage((current) => current.replace(/(^|\s)@\w*$/, `$1@${name} `));
  }

  return (
    <Autocomplete disableAutoFocusFirst filter={contains} inputValue={mentionQuery}>
      <TextField
        as="div"
        className="flex w-full max-w-xs flex-col gap-1.5"
        onChange={setMessage}
        value={message}
      >
        <Label className="text-base font-medium text-zinc-900 sm:text-sm dark:text-zinc-100">
          Message
        </Label>
        <TextArea
          className="min-h-28 w-full rounded border border-zinc-950/10 bg-white px-3 py-2.5 text-base text-zinc-950 outline-teal-600 focus-visible:outline-2 sm:py-2 sm:text-sm dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-50 dark:outline-teal-400"
          placeholder="Type @ to mention someone"
        />
        {mention && (
          <ListBox
            aria-label="Mention suggestions"
            className="rounded border border-zinc-950/10 bg-white p-1 dark:border-white/10 dark:bg-zinc-900"
            onChange={insertMention}
          >
            {teammates.map((teammate) => (
              <ListBoxItem
                key={teammate}
                className="cursor-pointer rounded px-3 py-2.5 text-base text-zinc-800 data-active:bg-teal-100 data-active:text-teal-950 data-selected:bg-teal-100 data-selected:text-teal-950 sm:py-2 sm:text-sm dark:text-zinc-100 dark:data-active:bg-teal-950 dark:data-active:text-teal-50 dark:data-selected:bg-teal-950 dark:data-selected:text-teal-50"
                value={teammate}
              >
                @{teammate}
              </ListBoxItem>
            ))}
          </ListBox>
        )}
      </TextField>
    </Autocomplete>
  );
}

Email recipients

Add matching contacts as recipient tokens and clear the query after each selection.

Mina Park
Aisha Rahmanaisha@example.com
Diego Santosdiego@example.com
Mina Parkmina@example.com
autocomplete.recipients.tsxtsx
import { useState } from "react";
import {
  Autocomplete,
  Label,
  ListBox,
  ListBoxItem,
  SearchField,
  SearchFieldInput,
} from "@comp0/react";

const contacts = [
  { email: "aisha@example.com", name: "Aisha Rahman" },
  { email: "diego@example.com", name: "Diego Santos" },
  { email: "mina@example.com", name: "Mina Park" },
];

function contains(textValue: string, inputValue: string) {
  return textValue.toLocaleLowerCase().includes(inputValue.trim().toLocaleLowerCase());
}

export function Example() {
  const [query, setQuery] = useState("");
  const [recipients, setRecipients] = useState<string[]>(["mina@example.com"]);

  function addRecipient(email: string) {
    setRecipients((current) => (current.includes(email) ? current : [...current, email]));
    setQuery("");
  }

  return (
    <Autocomplete filter={contains} inputValue={query} onInputChange={setQuery}>
      <SearchField as="div" className="flex w-full max-w-xs flex-col gap-1.5">
        <Label className="text-base font-medium text-zinc-900 sm:text-sm dark:text-zinc-100">
          Recipients
        </Label>
        <div className="flex flex-wrap gap-1 rounded border border-zinc-950/10 bg-white p-1.5 dark:border-white/10 dark:bg-zinc-900">
          {recipients.map((recipient) => (
            <span
              key={recipient}
              className="rounded bg-teal-100 px-2 py-1 text-sm text-teal-950 dark:bg-teal-950 dark:text-teal-50"
            >
              {contacts.find((contact) => contact.email === recipient)?.name}
            </span>
          ))}
          <SearchFieldInput
            aria-label="Add recipient"
            autoComplete="off"
            className="min-w-28 flex-1 bg-transparent px-1 py-1 text-base text-zinc-950 outline-none sm:text-sm dark:text-zinc-50"
            placeholder="Add a person"
          />
        </div>
        <ListBox
          aria-label="Matching contacts"
          className="rounded border border-zinc-950/10 bg-white p-1 dark:border-white/10 dark:bg-zinc-900"
          onChange={addRecipient}
        >
          {contacts.map((contact) => (
            <ListBoxItem
              key={contact.email}
              className="cursor-pointer rounded px-3 py-2.5 text-base text-zinc-800 data-active:bg-teal-100 data-active:text-teal-950 data-selected:bg-teal-100 data-selected:text-teal-950 sm:py-2 sm:text-sm dark:text-zinc-100 dark:data-active:bg-teal-950 dark:data-active:text-teal-50 dark:data-selected:bg-teal-950 dark:data-selected:text-teal-50"
              textValue={`${contact.name} ${contact.email}`}
              value={contact.email}
            >
              <span className="block">{contact.name}</span>
              <span className="block text-sm text-zinc-500 dark:text-zinc-400">
                {contact.email}
              </span>
            </ListBoxItem>
          ))}
        </ListBox>
      </SearchField>
    </Autocomplete>
  );
}

Anatomy

Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.

A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.

  1. Autocomplete

    Wrapper-free provider for one editable query and a composed collection. Does not add a DOM element.

  2. SearchField

    Editable query field; use TextField and TextArea for inline completion. Does not add a DOM element.

  3. Label

    Visible name connected to the query field. Owns a DOM element.

  4. SearchFieldInput

    Native search input that provides the completion query. Owns a DOM element.

  5. ListBox

    Selectable completion results; it owns selection state and callbacks. Owns a DOM element.

  6. ListBoxItem

    One selectable completion result. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Wrap the field and its result collection in Autocomplete; it adds no DOM element.

  2. 2

    Add the supporting parts

    Compose the field from SearchField and SearchFieldInput, then render results with ListBox and ListBoxItem.

  3. 3

    Make the behavior clear

    Pass filter to match item text. Handle collection selection or menu actions yourself—choosing a result never overwrites the query.

    Exampletsx
    <Autocomplete filter={contains}><SearchField><Label>Destination</Label><SearchFieldInput name="destination" /><ListBox aria-label="Destinations"><ListBoxItem value="paris">Paris</ListBoxItem></ListBox></SearchField></Autocomplete>

Keyboard

Moves virtual focus to the next matching result.
Moves virtual focus to the previous matching result.
Home
Keeps native caret movement and clears virtual focus.
End
Keeps native caret movement and clears virtual focus.
Returns to text editing and clears virtual focus.
Returns to text editing and clears virtual focus.
Selects the active listbox item or runs the active menu item.
Esc
Uses the composed field or menu’s normal Escape behavior.

Forms and accessibility

The composed SearchFieldInput or TextArea owns native name and form serialization. ListBox selection and MenuItem actions stay separate from the typed query.

Accessibility checklist

  • Give the text field a visible Label and give its ListBox or MenuList a clear name when the purpose is not evident.
  • Keep the active matching item mounted while virtual focus refers to it; use disableVirtualFocus when the collection should move DOM focus instead.
  • Make empty and loading messages non-selectable, and do not use a completion as the only way to enter a value.

API reference

Importtsx
import { Autocomplete, Label, ListBox, ListBoxItem, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, SearchField, SearchFieldInput, TextArea, TextField } from "@comp0/react";

Autocomplete

Context only

Wrapper-free provider for one editable query and a composed collection.

PropTypeDescription
childrenReactNodeA text field and its ListBox or Menu collection.
filter(textValue: string, inputValue: string) => booleanOptional client-side match rule for collection item text; omit it for externally filtered results.
inputValue / defaultInputValuestringControlled or initial completion query; an inline editor may pass only its current token.
onInputChange(value: string) => voidReceives the next typed text.
disableAutoFocusFirstbooleanLeaves virtual focus empty after the query changes.
disableVirtualFocusbooleanRestores the collection’s normal DOM-focus behavior.

SearchField

Context only

Editable query field; use TextField and TextArea for inline completion.

Label

DOM element

Visible name connected to the query field.

SearchFieldInput

DOM element

Native search input that provides the completion query.

PropTypeDescription
namestringSubmission name for the typed query.
placeholderstringHint text; never a replacement for Label.
autoCompletestringNative browser autofill hint; independent from application suggestions.

ListBox

DOM element

Selectable completion results; it owns selection state and callbacks.

PropTypeDescription
valuestringControlled selected item key.
defaultValuestringInitial selected item key.
onChange(value: string) => voidReceives the selected completion key.
aria-labelstringNames results when no visible heading names them.

ListBoxItem

DOM element

One selectable completion result.

PropTypeDescription
valuestringCollection key passed to ListBox.onChange.
disabledbooleanExcludes this result from selection.
textValuestringMatching and typeahead text when rich children do not provide a plain label.

Style hooks

Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.

[aria-activedescendant]

on SearchFieldInput, TextArea

The query field points to the virtually focused completion.
[aria-controls]

on SearchFieldInput, TextArea

The query field points at its composed completion collection.
[aria-autocomplete]

on SearchFieldInput, TextArea

The field advertises its list of text-dependent completions.
[data-active]

on ListBoxItem, MenuItem

The item has the virtual keyboard highlight.
[data-selected]

on ListBoxItem

The collection selected this completion.
[data-disabled]

on ListBoxItem, MenuItem

The completion cannot be selected or activated.

Keep exploring