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).

Pick one or type your own.

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-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13

Then 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.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/combobox

Its 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.13
import { 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 Select instead for a short list people don't need to search, and TextField for 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 pass onInputChange, 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 is aria-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; multiple submits 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 items as you give them, without filtering, and you set loading while a search runs. Keep the picked items in items, so their labels stay known.
placeholder
Type: string
An example shown while the input is empty: "Search herbs". It is not a label; pass label.
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 label in 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 with multiple.

Multiple: ComboboxMultipleProps

A Combobox that picks several items, shown as chips in the input.

allowCustomValue
Type: false
Not with multiple: allowCustomValue keeps 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 with allowCustomValue.

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 null when cleared.
value
Type: string | null
The picked item's value, when you control it; null is nothing picked.

Not in this mode (typed never):

  • customValueLabel: Only with allowCustomValue.
  • removeLabel: Only with multiple.

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 uses allowCustomValue: <Combobox allowCustomValue>.
  • errorMessage: Not a prop. Ouro UI uses error: <Combobox error="Required.">.
  • freeSolo: Not a prop. Ouro UI uses allowCustomValue: <Combobox allowCustomValue>.
  • helperText: Not a prop. Ouro UI uses description: <Combobox description="...">.
  • invalid: Not a prop. error marks 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 uses disabled: <Combobox disabled>.
  • isInvalid: Not a prop. error marks the field invalid: <Combobox error="What is wrong">.
  • isLoading: Not a prop. Ouro UI uses loading: <Combobox loading>.
  • isMulti: Not a prop. Ouro UI uses multiple: <Combobox multiple>.
  • isRequired: Not a prop. Ouro UI uses required: <Combobox required>.
  • noOptionsMessage: Not a prop. Ouro UI uses emptyLabel: <Combobox emptyLabel="No herbs">.
  • onChange: Not a prop. Combobox reports the pick itself: <Combobox onValueChange={(value) => …}>.
  • onInputValueChange: Not a prop. Ouro UI uses onInputChange: <Combobox onInputChange={(text) => …}>.
  • onSearch: Not a prop. Ouro UI uses onInputChange: <Combobox onInputChange={(text) => …}>.
  • options: Not a prop. Ouro UI uses items: <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