A text input that filters a list of items as people type, on the Field contract: pass label, items, and error from your server action or client check. It picks one item, several as chips with multiple, or accepts any text with allowCustomValue. Pass onInputChange and loading to search on your server. A clear button shows while there is a value. On a phone (below 640px with touch) the field is a button that opens a full-height bottom sheet with the search input pinned at the top, and a clear button sits beside it. With allowCustomValue the sheet's first row keeps what was typed (customValueLabel). The sheet opens with focus on itself, not the search input, so the keyboard does not cover the list until people tap the search (as Base UI does for touch).
On this site
This site's own pages use Combobox too. Open one with X-ray pointing it out: The Nautilus's story
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/combobox#v1.3.10Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/comboboxIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/combobox#v1.3.10 ourostack/ouro-ui/combobox-tests#v1.3.10import { Combobox } from "@/components/ouro/combobox/combobox";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<Combobox label="Herb" name="herb" items={["Basil", "Dill", "Thyme"]} />
<Combobox label="Tags" name="tags" multiple items={tags} defaultValue={["Quick"]} />
<Combobox label="Recipe" items={results} loading={searching} onInputChange={search} />
<Combobox label="Cuisine" name="cuisine" allowCustomValue items={["Italian", "Thai"]} />Use instead when
- Use
Selectinstead for a short list people don't need to search, andTextFieldfor free text with no suggestions.
Props
The props the modes share. Type: ComboboxProps.
autoHighlight- Type:
boolean - The first item that matches is highlighted as people type, so Enter picks it.
className- Type:
string - Layout only: margins, width, grid placement (
mt-4,col-span-2). Appearance classes warn in development; style fields in your theme. clearLabel- Type:
string - Default:
"Clear" - The name of the button that clears the field, as visually hidden text and not an
aria-label, so a test's label lookup for the field finds the field alone. It shows while there is something to clear. Translate it with your app. description- Type:
ReactNode - Supporting text under the label, read after the label through
aria-describedby. disabled- Type:
boolean - Disables the field and its buttons, and fades it.
doneLabel- Type:
string - Default:
"Done" - The label of the button that closes the sheet on a phone (a visible button with
multiple). Translate it with your app. emptyLabel- Type:
string - Default:
"No results" - The text the list shows when nothing matches. Translate it with your app.
error- Type:
ReadonlyErrorMessages - What is wrong, from your server action or client check. The message (or several, as a list) shows under the field, which gets
aria-invalid. A message is text or a React node (one with a link); an empty string or array means valid. hideLabel- Type:
boolean - Hides the label visually; screen readers still read it as the field's name.
items(required)- Type:
readonly ComboboxItem[] - What people pick from: strings, or
{ value, label }objects. Combobox filters them as people type, unless you passonInputChange, which makes them your search results. label(required)- Type:
string - What the field is for, shown above it and read as its name. Required: every control needs a name. To hide it visually, keep it and add
hideLabel. A placeholder is not a label. loading- Type:
boolean - A search is running: the list shows a spinner and
loadingLabel, and isaria-busy. loadingLabel- Type:
string - Default:
"Loading…" - The text the list shows while
loading. Translate it with your app. name- Type:
string - The form field name the picked value submits under;
multiplesubmits one entry per pick. onInputChange- Type:
((text: string) => void) - Called with the text as people type. Pass it to search on your server: Combobox then shows
itemsas you give them, without filtering, and you setloadingwhile a search runs. Keep the picked items initems, so their labels stay known. onInputKeyDown- Type:
((event: KeyboardEvent<HTMLInputElement>) => void) - Called on each key pressed in the input, before Combobox acts on it. Call
event.preventDefault()to stop Combobox's own handling of that key (Enter picking, Tab, the arrows), such as to move on from an empty input on Enter. placeholder- Type:
string - An example shown while the input is empty:
"Search herbs". It is not a label; passlabel. readOnly- Type:
boolean - People can see the field and what is picked but not change it: the input takes no text, nothing in the list can be picked, and the clear and remove buttons go. The list still opens, to browse.
ref- Type:
Ref<HTMLInputElement> - The input element. On a phone, it is the search input in the sheet, while the sheet is open.
required- Type:
boolean - Something must be picked: the input gets
aria-required, and the label a marker.
Modes
ComboboxProps is a union: pass the props of exactly one mode. Mixing them is a type error.
Custom: ComboboxCustomProps
A Combobox that accepts any text, suggesting matching items as people type.
allowCustomValue(required)- Type:
true - Accept any text: the value is what is in the input, and items are suggestions.
customValueLabel- Type:
((text: string) => string) - Default:
(text) => `Use “${text}”` - The sheet's row (on a phone) that keeps what was typed, from the text. Translate it with your app.
defaultValue- Type:
string - The text at first, when the field keeps its own.
multiple- Type:
false - Not with
allowCustomValue: it keeps one typed value. onValueChange- Type:
((value: string) => void) - Called with the text as people type or pick a suggestion. A suggestion puts its
labelin the input, and the text is the value, so an{ value, label }item submits its label. value- Type:
string - The text, when you control it.
Not in this mode (typed never):
inputValue: Not withallowCustomValue: the text is the value, whichvaluecontrols.removeLabel: Only withmultiple.
Multiple: ComboboxMultipleProps
A Combobox that picks several items, shown as chips in the input.
allowCustomValue- Type:
false - Not with
multiple:allowCustomValuekeeps one typed value. defaultValue- Type:
string[] - The items picked at first, when the field keeps its own.
inputValue- Type:
string - The input's text, when you control it with
onInputChange. Set it to""inonValueChangeto clear the input after a pick. multiple(required)- Type:
true - Pick several items, shown as chips; the value is an array.
onValueChange- Type:
((value: string[]) => void) - Called with the picked items' values.
removeLabel- Type:
((label: string) => string) - Default:
(label) => `Remove ${label}` - The name of a chip's remove button, from the item's label, as visually hidden text and not an
aria-label. Translate it with your app. value- Type:
string[] - The picked items' values, when you control them.
Not in this mode (typed never):
customValueLabel: Only withallowCustomValue.
Single: ComboboxSingleProps
A Combobox that picks one item: its value, or null when nothing is picked.
allowCustomValue- Type:
false - Accept any text, not only an item.
defaultValue- Type:
string | null - The item picked at first, when the field keeps its own.
inputValue- Type:
string - The input's text, when you control it with
onInputChange. Set it to""inonValueChangeto clear the input after a pick. multiple- Type:
false - Pick several items, shown as chips.
onValueChange- Type:
((value: string | null) => void) - Called with the picked item's value, or
nullwhen cleared. value- Type:
string | null - The picked item's value, when you control it;
nullis nothing picked.
Not in this mode (typed never):
customValueLabel: Only withallowCustomValue.removeLabel: Only withmultiple.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
creatable: Not a prop. Ouro UI usesallowCustomValue:<Combobox allowCustomValue>.errorMessage: Not a prop. Ouro UI useserror:<Combobox error="Required.">.freeSolo: Not a prop. Ouro UI usesallowCustomValue:<Combobox allowCustomValue>.helperText: Not a prop. Ouro UI usesdescription:<Combobox description="...">.invalid: Not a prop.errormarks the field invalid:<Combobox error="What is wrong">.isClearable: Not a prop. The clear button shows whenever there is something to clear.isDisabled: Not a prop. Ouro UI usesdisabled:<Combobox disabled>.isInvalid: Not a prop.errormarks the field invalid:<Combobox error="What is wrong">.isLoading: Not a prop. Ouro UI usesloading:<Combobox loading>.isMulti: Not a prop. Ouro UI usesmultiple:<Combobox multiple>.isRequired: Not a prop. Ouro UI usesrequired:<Combobox required>.noOptionsMessage: Not a prop. Ouro UI usesemptyLabel:<Combobox emptyLabel="No herbs">.onChange: Not a prop. Combobox reports the pick itself:<Combobox onValueChange={(value) => …}>.onInputValueChange: Not a prop. Ouro UI usesonInputChange:<Combobox onInputChange={(text) => …}>.onSearch: Not a prop. Ouro UI usesonInputChange:<Combobox onInputChange={(text) => …}>.options: Not a prop. Ouro UI usesitems:<Combobox items={["Basil", "Dill"]}>.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: Combobox merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Types
ComboboxItem
An item to pick: a string, which is both what shows and what submits, or an object with the value that submits and the label that shows. disabled items show but can't be picked. An object can also show a picture, a second line and a mark, as a people picker does: media (an Avatar), description and meta (a Badge).
type ComboboxItem = string | ComboboxItemObject;ComboboxItemObject
An item as an object: what it submits, what it shows, and what shows around it.
interface ComboboxItemObject {
/** What the item submits, and what `value` and `onValueChange` hold. */
value: string;
/** What the item shows, on its row and on its chip with `multiple`; screen readers hear it first. */
label: string;
/** The item shows but can't be picked. */
disabled?: boolean | undefined;
/**
* A second, muted line under the label, such as a person's job title or email. It stays on
* one line, cut with an ellipsis, and shows whole on hover (its `title`).
*/
description?: string | undefined;
/**
* A picture before the label, on the row and, with `multiple`, on the chip: an `Avatar` with
* `alt=""` (the label already names it) and `size="xs"`, which fits a chip.
*/
media?: ReactNode;
/** A small mark at the row's end, such as a `<Badge>` that says "1:1". */
meta?: ReactNode;
}ErrorMessage
One error message: text, or a React element for a message that carries a link or other markup.
type ErrorMessage = string | ReactElement;ReadonlyErrorMessages
A read-only message view accepted by fields and Form; mutable input arrays remain valid.
type ReadonlyErrorMessages = ErrorMessage | readonly ErrorMessage[];Claim sources
- layout-classes: test:registry/ouro/components/ouro/combobox/combobox.test.tsx::Combobox > lets app layout classes win on the root
verify:testExists(registry/ouro/components/ouro/combobox/combobox.test.tsx::Combobox > lets app layout classes win on the root)
