Listbox
Listboxes present a scrollable set of options with single or multiple selection. Standalone, they are an inline chooser. Inside select and combobox, they are the embedded option surface for the popup.
Try it
Mental model
<rui-listbox> is a behavior host: RuiListboxOption children stay in parent JSX. The custom element coordinates aria-selected, roving tabindex, and rui-change. When embedded is set, the parent (select or combobox) owns selection and keyboard; the listbox only paints options.
The live value property is always a string[] ([] when nothing is selected). The HTML attribute is those tokens as a comma-separated string. JSX may pass a string or an array; a string is parsed as CSV. rui-change emits { value: string[] }. Commas in the attribute are delimiters.
Not form-associated. A standalone listbox wrapped in RuiField is available to RuiForm onSubmit only. An embedded listbox is the parent select or combobox's option surface, not a field control.
Usage
Add RuiListboxOption children with unique value props, or pass options for the default composition.
import { RuiListbox, RuiListboxOption } from '@ecopages/radiant-ui/listbox';
<RuiListbox value="cat" label="Animal">
<RuiListboxOption value="cat">Cat</RuiListboxOption>
<RuiListboxOption value="dog">Dog</RuiListboxOption>
</RuiListbox><RuiListbox
value="cat"
label="Animal"
options={[
{ value: 'cat', label: 'Cat' },
{ value: 'dog', label: 'Dog' },
]}
/>Custom markup
<rui-listbox> coordinates any light-DOM tree that matches its query contract. The Rui* helpers stamp these targets; they are not required.
import '@ecopages/radiant-ui/listbox';
<rui-listbox value="cat" label="Animal">
<div role="listbox" class="rui-listbox rui-listbox--bordered">
<div role="option" data-value="cat" class="rui-listbox__option">Cat</div>
<div role="option" data-value="dog" class="rui-listbox__option">Dog</div>
</div>
</rui-listbox>BEM classes are presentation-only. Do not set aria-selected or tabIndex on options — the host owns those.
Embedded listboxes
Set embedded when the listbox lives inside a select or combobox popup. The parent then owns selection; border chrome is omitted so the popup can provide it. An embedded listbox is not a field control: place the parent select or combobox inside RuiField. bordered overrides that default.
Selection indicators
The options API adds the default check SVG only in multiple mode. Single-select uses the selected-option background alone. For a composed option in either mode, place RuiListboxOptionIndicator as a child and pass children to replace the check. Visibility follows aria-selected; the indicator is decorative.
import { RuiListboxOption, RuiListboxOptionIndicator } from '@ecopages/radiant-ui/listbox';
import { RuiIconCheck } from '@ecopages/radiant-ui/icons';
<RuiListboxOption value="cat" label="Cat">
Cat
<RuiListboxOptionIndicator>
<RuiIconCheck />
</RuiListboxOptionIndicator>
</RuiListboxOption>Set label when the option contains decorative nodes (emoji, icons). Without it, the accessible name is the option's text minus the indicator subtree.
Multiple selection
selectionMode="multiple" exposes aria-multiselectable="true". Click or Space/Enter toggles the focused option. Arrow keys move focus without changing the selection.
<RuiListbox
selectionMode="multiple"
value={['apple', 'banana']}
label="Favorite fruit"
options={[
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
]}
/>Theming
Listbox surfaces map to semantic surface and selection roles, never Tailwind palette steps.
| Part | CSS roles |
|---|---|
List surface (rui-listbox) | background |
| Bordered variant | border, radius-container |
| Option | on-background, radius-control, space-control-* |
| Option hover | surface-container-low |
| Active option (visual focus) | surface-container, on-background |
| Selected option | primary, on-primary |
| Focus ring | focus-ring |
Override at the theme layer (tokens/presets/colors/*.css). Embedded listboxes inherit the popup surface from the parent shell. Indicator layout uses :has([data-listbox-option-indicator]); there is no public CSS variable for the check glyph — replace it with helper children.
API
RuiListbox is a custom element (<rui-listbox>) around a role="listbox" surface. It is not form-associated.
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
value | string | omitted | Comma-separated selected tokens. Empty selection removes the attribute. The JS property is string[]. |
label | string | '' | Accessible name for the list. |
disabled | boolean | false | Disable all selection. |
selection-mode | single · multiple | single | Single or multiple selection. |
embedded | boolean | false | Parent-owned listbox: no border chrome, selection handled by the parent. |
bordered | boolean | follows embedded | Override the border (true standalone, false embedded). |
Light-DOM contract
| Target | Required | Host writes | Author owns |
|---|---|---|---|
[role="listbox"] | yes | id, aria-multiselectable (multiple mode) | the list node |
[role="option"] | yes | aria-selected, roving tabIndex | data-value, data-label, aria-disabled, hidden |
data-value | per option | — | selection identity; fallback trimmed text |
data-label | per option | — | accessible name when children include decorative nodes |
Do not set aria-selected or tabIndex on options. Nested hosts: none.
Events
| Event | Detail | Description |
|---|---|---|
rui-change | { value: string[] } | Fired when an option is selected. |
View helpers
| Component | Target stamped | Notes |
|---|---|---|
RuiListbox | <rui-listbox> + [role="listbox"] shell | Accepts options or children. |
RuiListboxOption | [role="option"], data-value, data-label | — |
RuiListboxOptionIndicator | [data-listbox-option-indicator] | Decorative; not queried by the host. |
CSS classes
| Class | Description |
|---|---|
.rui-listbox | Scrollable option list surface (role="listbox"). |
.rui-listbox--bordered | Bordered standalone listbox. |
.rui-listbox__option | Selectable list option (RuiListboxOption). |
.rui-listbox__option-indicator | Selected-state indicator inside an option. |
Theme roles
| Part | CSS variables consumed |
|---|---|
| List surface | --background |
| Bordered variant | --border, --radius-container |
| Option | --on-background, --radius-control, --space-control-x, --space-control-y |
| Option hover / active | --surface-container-low, --surface-container |
| Selected option | --primary, --on-primary |
| Focus ring | --focus-ring |
Accessibility
- Options expose
role="option"witharia-selectedfor the current choice. - Multiple mode exposes
aria-multiselectable="true". The visual indicator is decorative; selection remains onaria-selected. - Arrow keys move focus between options. In multiple mode they do not toggle; Space or Enter does.
- Provide a
labelor external heading so users know what the list represents.