Select
The custom select widget with the field styling. Convenience mode takes
options and builds the button and panel; compound mode takes
Select.Label, Select.Button, Select.Options and Select.Option.
import Select from '@zuilib/primitives/select'
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 / onValueChange arrays and joins the selected
labels with ", ". compareBy compares object values by a key or a
predicate.
In a Field
Inside a Field the button takes the field's Label
and FieldError through aria-labelledby / aria-describedby, and
invalid / disabled are inherited when unset.
Compound parts
Children replace the auto-built button and panel. Select.Option takes
icon, description and a render-prop child receiving
{focus, selected, disabled}; the check mark is still drawn by the part.
Props
SelectProps<T>:
| Prop | Type | Default | Description |
|---|---|---|---|
valuerequired | T | — | An array when multiple |
onValueChangerequired | (value: T) => void | — | |
options | Array<SelectOptionData<T>> | — | Each {value, label, disabled?, icon?, description?}. Auto-builds the button and panel; ignored when children are given |
placeholder | string | 'Select option' | |
multiple | boolean | false | value / onValueChange become arrays; selected labels are joined with ", " |
compareBy | keyof T | (a, b) => boolean | — | Compare object values by a key or a predicate; default is reference equality |
invalid | boolean | — | Defaults to the enclosing Field invalid |
disabled | boolean | — | Defaults to the enclosing Field / 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 | SelectAnchor | false | 'bottom start' | false renders the panel inline, no portal |
portal | boolean | true when anchored | only honoured with anchor={false}; every anchored panel is portalled. 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 |
|---|---|---|---|
Select.Label | passiveLabel?: boolean | — | passiveLabel does not focus the button on click. A <label for> inside a Field, a <div> wired by aria-labelledby elsewhere |
Select.Button | indicator?: ReactNode | false, autoFocus?: boolean | — | indicator replaces the chevron; false removes it. Plus button attributes |
Select.Options | anchor?, portal?, modal?, keepMounted? | anchor 'bottom start' | keepMounted keeps the panel mounted while closed |
Select.Option | value, disabled?, icon?, description?, children | — | children may be (state) => ReactNode |
Slots
| Slot | Element | Notes |
|---|---|---|
[data-slot="select"] | div | Root. Also data-size |
[data-slot="select-label"] | label | div | — |
[data-slot="select-button"] | button | data-open / data-hover / data-focus / data-disabled |
[data-slot="select-value"] | span | Selected labels or placeholder (convenience mode) |
[data-slot="select-value-icon"] | span | The selected option icon in the button |
[data-slot="select-chevron"] | svg | — |
[data-slot="select-options"] | div | The panel; portalled to <body> when anchored |
[data-slot="select-option"] | div | data-focus / data-selected / data-disabled |
[data-slot="select-option-icon"] | span | — |
[data-slot="select-option-content"] | span | Label and description column |
[data-slot="select-option-description"] | span | — |
[data-slot="select-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 |
--danger | Invalid border and ring |
--ring | Focus ring on the button |
--popover, --popover-foreground, --border, --shadow-2 | Panel surface |
--accent, --accent-foreground | Focused row |
--primary | Check mark |
--muted-foreground | Placeholder, chevron, descriptions |
Accessibility
- 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 danger border.disabledsetsdata-disabledand removes the button from interaction.- Inside a Field the button is named by
Labeland described byFieldDescription/FieldError. In compound mode outside a field, useSelect.Label(ansr-onlyone is fine). namerenders hidden inputs so native form submission andFormDatasee the value.
Related
- NativeSelect: the native
<select>when the browser picker is enough. - Combobox: the same panel with a text input for filtering.
- Field: label, description and error wiring.
- Theming: the anchored panel is portalled to
<body>; passanchor={false}to keep it in the subtree so overrides reach it.