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.
Example
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.
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.
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.
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.
Autocomplete
Wrapper-free provider for one editable query and a composed collection. Does not add a DOM element.
SearchField
Editable query field; use TextField and TextArea for inline completion. Does not add a DOM element.
Label
Visible name connected to the query field. Owns a DOM element.
SearchFieldInput
Native search input that provides the completion query. Owns a DOM element.
ListBox
Selectable completion results; it owns selection state and callbacks. Owns a DOM element.
ListBoxItem
One selectable completion result. Owns a DOM element.
Step by step
- 1
Add the main part
Wrap the field and its result collection in Autocomplete; it adds no DOM element.
- 2
Add the supporting parts
Compose the field from SearchField and SearchFieldInput, then render results with ListBox and ListBoxItem.
- 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
import { Autocomplete, Label, ListBox, ListBoxItem, Menu, MenuItem, MenuList, MenuPopover, MenuTrigger, SearchField, SearchFieldInput, TextArea, TextField } from "@comp0/react";Autocomplete
Context onlyWrapper-free provider for one editable query and a composed collection.
| Prop | Type | Description |
|---|---|---|
children | ReactNode | A text field and its ListBox or Menu collection. |
filter | (textValue: string, inputValue: string) => boolean | Optional client-side match rule for collection item text; omit it for externally filtered results. |
inputValue / defaultInputValue | string | Controlled or initial completion query; an inline editor may pass only its current token. |
onInputChange | (value: string) => void | Receives the next typed text. |
disableAutoFocusFirst | boolean | Leaves virtual focus empty after the query changes. |
disableVirtualFocus | boolean | Restores the collection’s normal DOM-focus behavior. |
SearchField
Context onlyEditable query field; use TextField and TextArea for inline completion.
Label
DOM elementVisible name connected to the query field.
SearchFieldInput
DOM elementNative search input that provides the completion query.
| Prop | Type | Description |
|---|---|---|
name | string | Submission name for the typed query. |
placeholder | string | Hint text; never a replacement for Label. |
autoComplete | string | Native browser autofill hint; independent from application suggestions. |
ListBox
DOM elementSelectable completion results; it owns selection state and callbacks.
| Prop | Type | Description |
|---|---|---|
value | string | Controlled selected item key. |
defaultValue | string | Initial selected item key. |
onChange | (value: string) => void | Receives the selected completion key. |
aria-label | string | Names results when no visible heading names them. |
ListBoxItem
DOM elementOne selectable completion result.
| Prop | Type | Description |
|---|---|---|
value | string | Collection key passed to ListBox.onChange. |
disabled | boolean | Excludes this result from selection. |
textValue | string | Matching 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.