Combobox
A combobox is a text input that filters a list of options. It is Headless
UI's Combobox in a styled frame: the input takes the field tokens, the
panel the bg-popover surface shared with Listbox, Menu and Popover. It is
generic over the value type and over multiple.
import Combobox from '@zuilib/components/combobox'
Convenience mode
Pass options and the component renders the input, the chevron button and
the list. Typing filters by a case-insensitive label match; the panel
shows emptyMessage when nothing matches.
Sizes
sm, md and lg share the control tokens with Input and Select. The
chevron sits half the control padding from the edge, so a padding retheme
moves both.
Multiple
multiple turns value and onChange into arrays. The input shows ''
by default; pass displayValue to summarise the selection.
In a FormItem
Inside a FormItem the input takes the field's id, so Label and
Description wire themselves up, and invalid / disabled are inherited
when left unset. Do not pass id inside a FormItem.
Compound mode
Omit options and compose the parts yourself. Filtering is then yours:
Combobox.Input reports the query through onChange. Object values need
by so equality survives re-renders.
Props
ComboboxProps<T, Multiple>:
| Prop | Type | Default | Description |
|---|---|---|---|
value | T | null (T[] when multiple) | — | Controlled value; null / [] for nothing selected |
defaultValue | T | null (T[] when multiple) | — | Uncontrolled initial value |
onChange | (value) => void | — | |
options | Array<{value: T; label: string; disabled?: boolean}> | — | Convenience mode. Labels are the React keys, keep them unique |
children | ReactNode | — | Compound mode; ignored when options is given |
placeholder | string | 'Search...' | |
multiple | boolean | false | Value and onChange become arrays |
by | keyof T | (a: T, b: T) => boolean | — | Object-value equality; reference equality by default |
size | 'sm' | 'md' | 'lg' | 'md' | |
fullWidth | boolean | true | false shrinks the root to the input |
invalid | boolean | — | Defaults to the enclosing FormItem's invalid; sets aria-invalid + data-invalid |
disabled | boolean | — | Leave unset inside a FormItem to inherit |
anchor | placement | {to, gap, offset, padding} | false | 'bottom start' | Panel placement; false renders an inline panel |
portal | boolean | true when anchored | only honoured with anchor={false}; Headless UI portals every anchored panel. Use anchor={false} to keep the panel in the subtree so token overrides reach it |
immediate | boolean | false | Open on focus |
onQueryChange | (query: string) => void | — | Every keystroke, and '' on close |
filter | false | (option, query) => boolean | case-insensitive label match | false for server-side filtering |
loading | boolean | false | Replaces the list with a spinner row |
loadingMessage | ReactNode | 'Loading…' | |
emptyMessage | ReactNode | 'No results found.' | Shown whenever no option renders |
renderOption | (option, {focus, selected, disabled}) => ReactNode | — | Custom option body (convenience mode) |
displayValue | (value) => string | option label (single) / '' (multiple) | Text shown in the input for the value |
name | string | — | Hidden inputs for native form submission |
form | string | — | |
id | string | — | Id of the input. Only outside a FormItem |
autoFocus | boolean | — | |
aria-label | string | — | Accessible name without a visible Label |
aria-describedby | string | — | Merged with the FormItem description ids |
className | string | — | Merged onto the root wrapper |
Parts
Combobox.Input:size?,displayValue?(value),onChange?(event),defaultValue?: stringplus input attributes (Headless UI ownsrole,aria-*,value).Combobox.Button:size?,childrenreplace the chevron. Hasaria-label="Toggle options"outside a Field.Combobox.Options:anchor?,portal?,static?,unmount?,hold?,modal?,transition?.Combobox.Option:value,disabled?,size?,childrenor(state) => ReactNode.
Parts inherit size, anchor, portal, invalid and loading from the root.
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="combobox"] | div | Root. Also data-size; Headless UI adds data-open, data-disabled, data-invalid |
[data-slot="combobox-input"] | input | — |
[data-slot="combobox-button"] | button | Chevron toggle, absolutely positioned at the right edge |
[data-slot="combobox-chevron"] | svg | Rotates while open |
[data-slot="combobox-options"] | div | The listbox panel |
[data-slot="combobox-option"] | div | — |
[data-slot="combobox-loading"] | div | Spinner row while loading |
[data-slot="combobox-empty"] | div | The emptyMessage row |
[data-slot="combobox-status"] | span | Visually hidden live region for loading / empty text |
Accessibility
- The input has
role="combobox"witharia-expanded,aria-controls,aria-autocompleteandaria-activedescendantmanaged by Headless UI; the panel isrole="listbox"and each rowrole="option"witharia-selected. - Arrow keys move the active option, Enter selects, Escape closes; Alt+Down opens without changing the selection. Typing filters.
- Inside a
FormItem,Labelgives the input its name andDescription/Messageland inaria-describedby. Outside one, passaria-label. invalidsetsaria-invalidand paints the input destructive; the loading and empty rows are disabled options and are also announced from thecombobox-statuslive region.- The chevron button is not in the tab order (Headless UI sets
tabIndex=-1); keyboard users open the list from the input.