Form
Combobox
Autocomplete text input following the ARIA combobox (list) pattern, with filtering, a custom filter predicate, free-text mode and an optional clear button.
Import
import { Combobox } from "@zephora/react";Examples
Basic
Type to filter; the default filter is a case-insensitive contains.
<Combobox
aria-label="Country"
placeholder="Search countries…"
options={[
{ label: "Türkiye", value: "tr" },
{ label: "Germany", value: "de" },
{ label: "France", value: "fr" },
{ label: "Japan", value: "jp" },
{ label: "Brazil", value: "br" },
]}
/>Controlled with clear
Value: (none)
const [lang, setLang] = React.useState("");
<Combobox
aria-label="Language"
value={lang}
onValueChange={setLang}
clearable
options={[
{ label: "TypeScript", value: "ts" },
{ label: "Rust", value: "rust" },
{ label: "Go", value: "go" },
{ label: "Python", value: "py" },
]}
/>
<p>Value: {lang || "(none)"}</p>Free text (freeSolo)
`freeSolo` lets Enter commit text that matches no option.
<Combobox
aria-label="Tag"
freeSolo
placeholder="Pick or type a tag…"
options={[
{ label: "bug", value: "bug" },
{ label: "feature", value: "feature" },
{ label: "docs", value: "docs" },
]}
/>Async search
`filterMode="none"` renders the options exactly as given (your code filters server-side); `loading` shows a spinner row while a request is in flight. This demo simulates a 400 ms request.
const ALL = ["Ankara", "Berlin", "Boston", "Lisbon", "London", "Madrid", "Tokyo"];
const [options, setOptions] = React.useState(ALL.map((c) => ({ label: c, value: c })));
const [loading, setLoading] = React.useState(false);
const search = (text: string) => {
setLoading(true);
// Replace with your fetch(); filtering happens on the server.
window.setTimeout(() => {
setOptions(
ALL.filter((c) => c.toLowerCase().includes(text.toLowerCase())).map((c) => ({
label: c,
value: c,
}))
);
setLoading(false);
}, 400);
};
<Combobox
aria-label="City"
placeholder="Search cities…"
filterMode="none"
loading={loading}
options={options}
onInputChange={search}
/>API
Combobox props
| Prop | Type | Default | Description |
|---|---|---|---|
options * | ComboboxOption[] | — | Options offered in the listbox. Same shape as SelectOption (label, value, disabled?, icon?, description?, group?, textValue?). |
value | string | — | Controlled selected value ("" = nothing selected). |
defaultValue | string | — | Initial selected value in uncontrolled mode. |
onValueChange | (value: string) => void | — | Called with the newly selected value. |
inputValue | string | — | Controlled text-field value. |
onInputChange | (inputValue: string) => void | — | Called on every text-field change. |
filterFn | (option, inputValue) => boolean | case-insensitive contains | Filter predicate applied to the options. |
filterMode | "client" | "none" | "client" | "client" filters options against the input text; "none" renders options as-is (server-side/async search — the parent filters). |
freeSolo | boolean | false | Allows committing free text (Enter) as the value. |
placeholder | string | — | Input placeholder text. |
size | "sm" | "md" | "lg" | "md" | Input scale. |
invalid | boolean | false | Marks the input invalid (aria-invalid). |
disabled | boolean | false | Disables the input. |
clearable | boolean | false | Shows a clear button when there is text or a selection. |
loading | boolean | false | Shows a spinner row in the popup and sets aria-busy on the listbox. |
renderOption | (option: ComboboxOption, state: { selected, active }) => ReactNode | — | Custom option renderer — replaces the default icon/label/description layout. |
unstyled | boolean | false | Headless mode — drops Zephora classes so your own CSS can style it. |
…rest | HTMLAttributes<HTMLDivElement> | — | Forwarded to the root wrapper element. |
Keyboard
| Key | Action |
|---|---|
Type | Filters the options and opens the listbox. |
Arrow Down / Arrow Up | Opens the list or moves the active option. |
Enter | Selects the active option (or commits free text with freeSolo). |
Escape | Closes the listbox. |
Tab | Closes the listbox and moves focus. |