Headless / Selection

Combobox

Combobox is a single-select with type-to-filter. An input narrows a listbox of options as the user types; the arrow keys and Enter pick one. It is the base Select, Search, and MultiSelect build on.

Loading playground…
Updated 3 days agoEdit on GitHubWeb & native

What it is

A compound component. Combobox.Root holds the options, the selected value, the input text, and the open state — every piece controllable or uncontrolled. Combobox.Input is the typeable field; Combobox.List is the filtered, portalled listbox. Filtering defaults to a case-insensitive substring match on each option's label; pass filter to override it.

Install

example.tsx
yarn add @usemotif/headless
example.tsx
import { Combobox } from '@usemotif/headless';

Anatomy

example.tsx
<Combobox.Root options={options} onValueChange={setValue}>
<Combobox.Input placeholder="Search…" />
<Combobox.List
  renderOption={(option, { highlighted }) => (
    <Row option={option} active={highlighted} />
  )}
/>
</Combobox.Root>

API

Combobox.Root

Combobox.Root

stable
function Combobox.Root<T>(props: ComboboxRootProps<T>): JSX.Element
optionsreadonly ComboboxOption<T>[]required

The full option set. Each option is `{ value, label, disabled? }`.

value / defaultValue / onValueChangeT / T / (next: T | undefined) => void

The selected value, controlled or uncontrolled.

inputValue / onInputValueChangestring / (next: string) => void

The input text. Control it to drive filtering from outside.

filter(option, input) => boolean

Custom filter predicate. Defaults to a case-insensitive substring match on `label`.

open / defaultOpen / onOpenChangeboolean / boolean / (open) => void

The listbox open state, controlled or uncontrolled.

Combobox.Input

The typeable field. Pass a placeholder, or a single element child to use your own input — Combobox clones it with the combobox ARIA wiring and the keyboard handlers.

Combobox.List

Combobox.List

stable
function Combobox.List<T>(props: ComboboxListProps<T>): JSX.Element | null
renderOption(option, info) => ReactNode

Renders one option row. `info` carries `highlighted`, `selected`, and `index`. Defaults to the option label.

placement / offsetPlacement / number

Where the listbox sits relative to the input, and the pixel gap.

emptyMessageReactNode= "No options"

Rendered when the filter matches nothing.

Keyboard

Arrow Down and Up move the highlight (Arrow Down also opens a closed listbox), Home and End jump to the ends, Enter selects the highlighted option, Escape closes the listbox.

Accessibility

Combobox implements the WAI-ARIA combobox pattern. The input is role="combobox" with aria-expanded, aria-controls, aria-autocomplete="list", and aria-activedescendant tracking the highlighted row. The listbox is role="listbox", each row role="option" with aria-selected. Disabled options carry aria-disabled and are skipped on selection.

Examples

A country picker:

example.tsx
const options = countries.map((c) => ({ value: c.code, label: c.name }));
 
<Combobox.Root options={options} onValueChange={setCountry}>
<Combobox.Input placeholder="Search countries…" />
<Combobox.List
  style={{ padding: 4, background: 'var(--colors-surface-base)' }}
  renderOption={(opt, { highlighted }) => (
    <Box bg={highlighted ? '$colors.surface.muted' : undefined} p="$2">
      {opt.label}
    </Box>
  )}
/>
</Combobox.Root>