Combobox
A searchable select: type to filter, pick one or many, or load options from a server as the user types.
const frameworks = [
{ value: "next", label: "Next.js" },
{ value: "remix", label: "Remix" },
{ value: "gatsby", label: "Gatsby", disabled: true },
{ value: "nuxt", label: "Nuxt", description: "Vue" },
];
<Field label="Framework">
<Combobox options={frameworks} placeholder="Search frameworks…" name="framework" />
</Field>Import
import { Combobox, type ComboboxOption } from "@meridui/react";Examples
Multiple
multiple keeps the list open after each pick and shows the selection as chips. value becomes a string[]. Backspace in an empty field removes the last chip.
Pick any number.
const [value, setValue] = useState<string[]>(["next", "vite"]);
<Field label="Stack" description="Pick any number.">
<Combobox multiple options={frameworks} value={value} onValueChange={setValue} placeholder="Add…" />
</Field>Async options
Fetch from onInputValueChange, pass the results as options, set filter={false} so the component does not filter again, and toggle loading while the request runs. Labels of chosen options are remembered, so chips and the field keep their text when the results change.
const [text, setText] = useState("");
const [options, setOptions] = useState<ComboboxOption[]>([]);
const [loading, setLoading] = useState(false);
useEffect(() => {
if (!text.trim()) return;
setLoading(true);
const timer = setTimeout(async () => {
setOptions(await searchCities(text));
setLoading(false);
}, 200);
return () => clearTimeout(timer);
}, [text]);
<Combobox
options={options}
filter={false}
loading={loading}
onInputValueChange={setText}
emptyMessage="No cities found"
/>Sizes and states
<Combobox aria-label="Small" size="sm" options={frameworks} />
<Combobox aria-label="Invalid" invalid options={frameworks} />
<Combobox aria-label="Disabled" disabled options={frameworks} defaultValue="astro" />API reference
Renders a div wrapper; other div attributes go on it. aria-label and aria-labelledby go on the text field. Inside a Field the field picks up the id, label, description, error, required and disabled.
| Prop | Type | Default | Description |
|---|---|---|---|
optionsRequired | ComboboxOption[] | – | { value, label, description?, disabled? }. Replace them as async results arrive. |
multiple | boolean | false | Select many values, shown as chips. |
value | string | string[] | – | Controlled value: a string ("" for none), or string[] with multiple. |
defaultValue | string | string[] | – | Initial value when uncontrolled. |
onValueChange | (value) => void | – | Called with the new value. |
inputValue | string | – | Controlled text in the field. |
defaultInputValue | string | – | Initial text. Defaults to the selected label. |
onInputValueChange | (text: string) => void | – | Called on every keystroke; fetch async options here. |
open / defaultOpen / onOpenChange | boolean / boolean / (open) => void | – | Controlled or initial open state of the list. |
filter | ((option, text) => boolean) | false | label contains text | Client-side filter. false when options are already filtered. |
locale | string | – | Locale for case-insensitive matching, e.g. "tr-TR" so İ and i match. |
loading | boolean | false | Shows loadingMessage and sets aria-busy on the list. |
placeholder | string | – | Placeholder of the text field. |
emptyMessage | ReactNode | "No results" | Shown when nothing matches. |
loadingMessage | ReactNode | "Loading…" | Shown while loading. |
toggleLabel | string | "Show options" | Accessible name of the chevron button. |
getRemoveLabel | (label: string) => string | Remove {label} | Accessible name of a chip's remove button. |
size | "sm" | "md" | "lg" | "md" | Control size. |
invalid | boolean | – | aria-invalid and the danger border. Inside a Field, derived from its error. |
disabled | boolean | – | Disables the combobox. |
required | boolean | – | Marks the field required. |
name | string | – | Form name. Renders one hidden input per selected value. |
inputId | string | – | id of the text field, for a <label htmlFor>. |
placement | Placement | "bottom-start" | Preferred list placement. |
container | Element | null | document.body | Portal target of the list. |
inputRef | Ref<HTMLInputElement> | – | Ref to the text field. |
ref | Ref<HTMLDivElement> | – | Ref to the wrapper. |
Styling
| Class | Element |
|---|---|
.mrd-combobox | Wrapper (data-size, data-state, data-multiple, data-invalid, data-disabled) |
.mrd-combobox__control | Bordered field holding chips, input and toggle |
.mrd-combobox__input, .mrd-combobox__toggle | Text field, chevron button |
.mrd-combobox__chip, .mrd-combobox__chip-remove | Selected value chip (multiple) |
.mrd-combobox__content, .mrd-combobox__listbox | Portalled popup and its listbox |
.mrd-combobox__item | Option (data-active, data-disabled, aria-selected) |
.mrd-combobox__status | Loading / empty message |
Accessibility
Follows the WAI-ARIA Combobox pattern with a listbox popup and list autocomplete. Focus stays in the text field; the highlighted option is exposed with aria-activedescendant. The loading and empty messages are a role="status" region, so they are announced. Chip remove buttons are skipped by Tab; use Backspace in the empty field, or the pointer.
| Key | Action |
|---|---|
| ArrowDown | Open the list, or highlight the next option (wraps). |
| ArrowUp | Open the list at the last option, or highlight the previous option (wraps). |
| AltArrowDown | Open without moving the highlight. |
| Enter | Select the highlighted option. Single: closes. Multiple: toggles and stays open. |
| Escape | Close the list; single mode restores the selected label. |
| Backspace | Multiple, empty field: remove the last chip. |
| Tab | Close and move on. |
Guidelines
Do
Use Combobox when the list is long (20+ items) or comes from a server, and people know what they are looking for.
Avoid