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.
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.
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.
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.
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.
Props
ListboxProps<T>:
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | T | — | An array when multiple |
onChangerequired | (value: T) => void | — | |
options | Array<{value, label, disabled?, icon?, description?}> | — | Auto-builds the button and panel; ignored when children are given |
placeholder | string | 'Select option' | |
multiple | boolean | false | value / onChange become arrays; selected labels are joined with ", " |
by | keyof T | (a, b) => boolean | — | Compare object values by a key or a predicate; default is reference equality |
invalid | boolean | — | Defaults to the enclosing FormItem invalid |
disabled | boolean | — | Defaults to the enclosing FormItem / Fieldset disabled |
name | string | — | Renders hidden inputs so the value submits with a native form |
form | string | — | The form the hidden inputs belong to |
size | 'sm' | 'md' | 'lg' | 'md' | |
fullWidth | boolean | true | false renders the root inline-block so it shrinks to the button |
anchor | Headless UI anchor | false | 'bottom start' | false renders the panel inline, no portal |
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 and .dark reach it |
renderOption | (option, {focus, selected, disabled}) => ReactNode | — | Custom option body; the check mark is still drawn |
className | string | — | Merged last onto the root |
children | ReactNode | — | Compound mode |
Parts
Parts inherit size, invalid and disabled from the root.
| Prop | Type | Default | Description |
|---|---|---|---|
Listbox.Label | passive?: boolean | — | passive does not focus the button on click. A <label for> inside a FormItem, a <div> wired by aria-labelledby elsewhere |
Listbox.Button | hideChevron?: boolean, autoFocus?: boolean | — | Plus button attributes |
Listbox.Options | anchor?, portal?, modal?, static?, unmount? | anchor 'bottom start' | static always renders; unmount keeps the panel mounted while closed |
Listbox.Option | value, disabled?, icon?, description?, children | — | children may be (state) => ReactNode |
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="listbox"] | div | Root. Also data-size |
[data-slot="listbox-label"] | label | div | — |
[data-slot="listbox-button"] | button | Headless UI data-open / data-hover / data-focus / data-disabled |
[data-slot="listbox-value"] | span | Selected labels or placeholder (convenience mode) |
[data-slot="listbox-value-icon"] | span | The selected option icon in the button |
[data-slot="listbox-chevron"] | svg | — |
[data-slot="listbox-options"] | div | The panel; portalled to <body> when anchored |
[data-slot="listbox-option"] | div | Headless UI data-focus / data-selected / data-disabled |
[data-slot="listbox-option-icon"] | span | — |
[data-slot="listbox-option-content"] | span | Label and description column |
[data-slot="listbox-option-description"] | span | — |
[data-slot="listbox-option-check"] | svg | Visible only when selected |
Tokens
| Token | Used for |
|---|---|
--control-height-sm/md/lg, --control-padding-x-* | Button box and row insets |
--input, --background, --foreground | Button field frame |
--destructive | Invalid border and ring |
--ring | Focus ring on the button |
--popover, --popover-foreground, --border, --shadow-* | Panel surface |
--accent, --accent-foreground | Focused row |
--primary | Check mark |
--muted-foreground | Placeholder, chevron, descriptions |
Accessibility
- Headless UI roles: the button has
aria-haspopup="listbox"andaria-expanded; the panel isrole="listbox"withrole="option"rows andaria-selected;aria-multiselectablewhenmultiple. - Keyboard:
Enter,Space,ArrowUp/ArrowDownopen the panel; arrows move focus;Home/Endjump; typing does typeahead;Enter/Spaceselect;Escapecloses and returns focus to the button. invalidsetsaria-invalidon the button and the destructive border.disabledsetsdata-disabledand removes the button from interaction.- Inside a FormItem the button is named by
Labeland described byDescription/Message. In compound mode outside a field, useListbox.Label(ansr-onlyone is fine). namerenders hidden inputs so native form submission andFormDatasee the value.