Skip to main content

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.

Loading example

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.

Loading example

Multiple

multiple turns value and onChange into arrays. The input shows '' by default; pass displayValue to summarise the selection.

Loading example

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.

Loading example

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.

Loading example

Props

ComboboxProps<T, Multiple>:

PropTypeDefaultDescription
valueT | null (T[] when multiple)Controlled value; null / [] for nothing selected
defaultValueT | null (T[] when multiple)Uncontrolled initial value
onChange(value) => void
optionsArray<{value: T; label: string; disabled?: boolean}>Convenience mode. Labels are the React keys, keep them unique
childrenReactNodeCompound mode; ignored when options is given
placeholderstring'Search...'
multiplebooleanfalseValue and onChange become arrays
bykeyof T | (a: T, b: T) => booleanObject-value equality; reference equality by default
size'sm' | 'md' | 'lg''md'
fullWidthbooleantruefalse shrinks the root to the input
invalidbooleanDefaults to the enclosing FormItem's invalid; sets aria-invalid + data-invalid
disabledbooleanLeave unset inside a FormItem to inherit
anchorplacement | {to, gap, offset, padding} | false'bottom start'Panel placement; false renders an inline panel
portalbooleantrue when anchoredonly 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
immediatebooleanfalseOpen on focus
onQueryChange(query: string) => voidEvery keystroke, and '' on close
filterfalse | (option, query) => booleancase-insensitive label matchfalse for server-side filtering
loadingbooleanfalseReplaces the list with a spinner row
loadingMessageReactNode'Loading…'
emptyMessageReactNode'No results found.'Shown whenever no option renders
renderOption(option, {focus, selected, disabled}) => ReactNodeCustom option body (convenience mode)
displayValue(value) => stringoption label (single) / '' (multiple)Text shown in the input for the value
namestringHidden inputs for native form submission
formstring
idstringId of the input. Only outside a FormItem
autoFocusboolean
aria-labelstringAccessible name without a visible Label
aria-describedbystringMerged with the FormItem description ids
classNamestringMerged onto the root wrapper

Parts

  • Combobox.Input: size?, displayValue?(value), onChange?(event), defaultValue?: string plus input attributes (Headless UI owns role, aria-*, value).
  • Combobox.Button: size?, children replace the chevron. Has aria-label="Toggle options" outside a Field.
  • Combobox.Options: anchor?, portal?, static?, unmount?, hold?, modal?, transition?.
  • Combobox.Option: value, disabled?, size?, children or (state) => ReactNode.

Parts inherit size, anchor, portal, invalid and loading from the root.

Slots

SlotElementNotes
[data-slot="combobox"]divRoot. Also data-size; Headless UI adds data-open, data-disabled, data-invalid
[data-slot="combobox-input"]input
[data-slot="combobox-button"]buttonChevron toggle, absolutely positioned at the right edge
[data-slot="combobox-chevron"]svgRotates while open
[data-slot="combobox-options"]divThe listbox panel
[data-slot="combobox-option"]div
[data-slot="combobox-loading"]divSpinner row while loading
[data-slot="combobox-empty"]divThe emptyMessage row
[data-slot="combobox-status"]spanVisually hidden live region for loading / empty text

Accessibility

  • The input has role="combobox" with aria-expanded, aria-controls, aria-autocomplete and aria-activedescendant managed by Headless UI; the panel is role="listbox" and each row role="option" with aria-selected.
  • Arrow keys move the active option, Enter selects, Escape closes; Alt+Down opens without changing the selection. Typing filters.
  • Inside a FormItem, Label gives the input its name and Description / Message land in aria-describedby. Outside one, pass aria-label.
  • invalid sets aria-invalid and paints the input destructive; the loading and empty rows are disabled options and are also announced from the combobox-status live region.
  • The chevron button is not in the tab order (Headless UI sets tabIndex=-1); keyboard users open the list from the input.