Skip to main content

Listbox

Headless UI Listbox with the field styling. Convenience mode takes options and builds the button and panel; compound mode takes Listbox.Label, Listbox.Button, Listbox.Options and Listbox.Option.

import Listbox from '@zuilib/components/listbox'

Options

Each option has value, label and optional icon, description, disabled. The selected option's icon is shown in the button.

Loading example

Sizes

sm, md and lg size the button from --control-height-* and the rows and icons to match. Parts inherit the size from the root.

Loading example

Multiple and object values

multiple makes value / onChange arrays and joins the selected labels with ", ". by compares object values by a key or a predicate.

Loading example

In a FormItem

Inside a FormItem the button takes the item's Label and Message through aria-labelledby / aria-describedby, and invalid / disabled are inherited when unset.

Loading example

Compound parts

Children replace the auto-built button and panel. Listbox.Option takes icon, description and a render-prop child receiving {focus, selected, disabled}; the check mark is still drawn by the part.

Loading example

Props

ListboxProps<T>:

PropTypeDefaultDescription
valuerequiredTAn array when multiple
onChangerequired(value: T) => void
optionsArray<{value, label, disabled?, icon?, description?}>Auto-builds the button and panel; ignored when children are given
placeholderstring'Select option'
multiplebooleanfalsevalue / onChange become arrays; selected labels are joined with ", "
bykeyof T | (a, b) => booleanCompare object values by a key or a predicate; default is reference equality
invalidbooleanDefaults to the enclosing FormItem invalid
disabledbooleanDefaults to the enclosing FormItem / Fieldset disabled
namestringRenders hidden inputs so the value submits with a native form
formstringThe form the hidden inputs belong to
size'sm' | 'md' | 'lg''md'
fullWidthbooleantruefalse renders the root inline-block so it shrinks to the button
anchorHeadless UI anchor | false'bottom start'false renders the panel inline, no portal
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 and .dark reach it
renderOption(option, {focus, selected, disabled}) => ReactNodeCustom option body; the check mark is still drawn
classNamestringMerged last onto the root
childrenReactNodeCompound mode

Parts

Parts inherit size, invalid and disabled from the root.

PropTypeDefaultDescription
Listbox.Labelpassive?: booleanpassive does not focus the button on click. A <label for> inside a FormItem, a <div> wired by aria-labelledby elsewhere
Listbox.ButtonhideChevron?: boolean, autoFocus?: booleanPlus button attributes
Listbox.Optionsanchor?, portal?, modal?, static?, unmount?anchor 'bottom start'static always renders; unmount keeps the panel mounted while closed
Listbox.Optionvalue, disabled?, icon?, description?, childrenchildren may be (state) => ReactNode

Slots

SlotElementNotes
[data-slot="listbox"]divRoot. Also data-size
[data-slot="listbox-label"]label | div
[data-slot="listbox-button"]buttonHeadless UI data-open / data-hover / data-focus / data-disabled
[data-slot="listbox-value"]spanSelected labels or placeholder (convenience mode)
[data-slot="listbox-value-icon"]spanThe selected option icon in the button
[data-slot="listbox-chevron"]svg
[data-slot="listbox-options"]divThe panel; portalled to <body> when anchored
[data-slot="listbox-option"]divHeadless UI data-focus / data-selected / data-disabled
[data-slot="listbox-option-icon"]span
[data-slot="listbox-option-content"]spanLabel and description column
[data-slot="listbox-option-description"]span
[data-slot="listbox-option-check"]svgVisible only when selected

Tokens

TokenUsed for
--control-height-sm/md/lg, --control-padding-x-*Button box and row insets
--input, --background, --foregroundButton field frame
--destructiveInvalid border and ring
--ringFocus ring on the button
--popover, --popover-foreground, --border, --shadow-*Panel surface
--accent, --accent-foregroundFocused row
--primaryCheck mark
--muted-foregroundPlaceholder, chevron, descriptions

Accessibility

  • Headless UI roles: the button has aria-haspopup="listbox" and aria-expanded; the panel is role="listbox" with role="option" rows and aria-selected; aria-multiselectable when multiple.
  • Keyboard: Enter, Space, ArrowUp / ArrowDown open the panel; arrows move focus; Home / End jump; typing does typeahead; Enter / Space select; Escape closes and returns focus to the button.
  • invalid sets aria-invalid on the button and the destructive border. disabled sets data-disabled and removes the button from interaction.
  • Inside a FormItem the button is named by Label and described by Description / Message. In compound mode outside a field, use Listbox.Label (an sr-only one is fine).
  • name renders hidden inputs so native form submission and FormData see the value.
  • Select: the native <select> when the browser picker is enough.
  • Combobox: the same panel with a text input for filtering.
  • FormItem: label, description and message wiring.
  • Theming: the anchored panel is portalled to <body>; pass anchor={false} to keep it in the subtree so overrides reach it.