Forms
Combobox
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).
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.2.13Then 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.2.13Or, 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.2.13 ourostack/ouro-ui/combobox-tests#v1.2.13import { 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.
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. placeholder- Type:
string - An example shown while the input is empty:
"Search herbs". It is not a label; passlabel. 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):
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.
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.
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.
type ComboboxItem = string | { value: string; label: string; disabled?: boolean | undefined };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)