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 Select, Menu and Popover. It is generic over the value type and over multiple.

import Combobox from '@zuilib/primitives/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 onValueChange into arrays. The input shows '' by default; pass displayValue to summarise the selection.

Loading example

In a Field

Inside a Field the input takes the field's id, so Label and FieldDescription wire themselves up, and invalid / disabled are inherited when left unset. Do not pass id inside a Field.

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 compareBy 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
onValueChange(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 onValueChange become arrays
compareBykeyof 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 Field's invalid; sets aria-invalid + data-invalid
disabledbooleanLeave unset inside a Field 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
openOnFocusbooleanfalseOpen 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 Field
autoFocusboolean
aria-labelstringAccessible name without a visible Label
aria-describedbystringMerged with the Field description ids
toggleLabelstring'Toggle options'Accessible name of the chevron button; the default comes from Labels.toggleOptions
trackstringNames the select telemetry event; falls back to name, then aria-label
classNamestringMerged onto the root wrapper

placeholder, loadingMessage and emptyMessage default to the Labels strings search, loadingMessage and noResults, so a LabelsProvider translates them once.

Parts

  • Combobox.Input: size?, displayValue?(value), onChange?(event), defaultValue?: string plus input attributes (Headless UI owns role, aria-*, value).
  • Combobox.Button: size?, toggleLabel?, children replace the chevron. Named toggleLabel (root prop, part prop or Labels.toggleOptions, default Toggle options) outside a Field.
  • Combobox.Options: anchor?, portal?, keepMounted?, keepHighlightOnPointerLeave?, 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 Field, Label gives the input its name and FieldDescription / FieldError land in aria-describedby. Outside one, pass aria-label.
  • invalid sets aria-invalid and paints the input with the danger colour; 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.