---
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';
```
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.