--- title: Listbox description: Listboxes present a scrollable set of options with single or multiple selection. They are the option surface for select, combobox, and autocomplete popups. category: Forms --- import { meta as ListboxMeta, Default, Multiple } from '@/content/stories/listbox'; import Canvas from '@/components/component-docs/canvas'; import Demo from '@/components/component-docs/demo'; export const config = { dependencies: { components: [Canvas, Demo], scripts: [ '../../components/component-docs/demo.script.tsx', '../../components/component-docs/canvas.script.tsx', '../../components/component-docs/controls.script.tsx', ], }, }; # 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 `` 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. ```tsx import { RuiListbox, RuiListboxOption } from '@ecopages/radiant-ui/listbox'; Cat Dog ``` ```tsx ``` ## Custom markup `` coordinates any light-DOM tree that matches its query contract. The `Rui*` helpers stamp these targets; they are not required. ```tsx import '@ecopages/radiant-ui/listbox';
Cat
Dog
``` 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. ```tsx import { RuiListboxOption, RuiListboxOptionIndicator } from '@ecopages/radiant-ui/listbox'; import { RuiIconCheck } from '@ecopages/radiant-ui/icons'; Cat ``` 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. ```tsx ``` ## 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 (``) 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` | `` + `[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"` with `aria-selected` for the current choice. - Multiple mode exposes `aria-multiselectable="true"`. The visual indicator is decorative; selection remains on `aria-selected`. - Arrow keys move focus between options. In multiple mode they do not toggle; Space or Enter does. - Provide a `label` or external heading so users know what the list represents.