Select
Selects let users choose one or more known values from a list inside a compact trigger and popup listbox. Use a select when the value must be one of the options; use a combobox when users also need to type and filter.
Try it
Mental model
<rui-select> is a behavior host: helpers place the trigger, value, and popup; the custom element owns open state, keyboard, and selection. Options live in an embedded RuiListbox. Named regions are Composition Helpers (RuiSelectTrigger, RuiSelectClear, …), not HTML slots.
The live value property is always a string[] ([] when nothing is selected). The HTML attribute is those tokens as a comma-separated string (value="draft,published"). 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 characters inside a token.
Not form-associated. Wrap in RuiField and read the value from RuiForm onSubmit. It does not appear in new FormData(form).
Usage
Pass options for the default composition, or supply children for an explicit layout.
import { RuiSelect } from '@ecopages/radiant-ui/select';
import { RuiLabel } from '@ecopages/radiant-ui/label';
<RuiLabel>Animal</RuiLabel>
<RuiSelect
value="cat"
placeholder="Select an animal"
options={[
{ value: 'cat', label: 'Cat' },
{ value: 'dog', label: 'Dog' },
]}
/>The options API does not include a clear control. Add RuiSelectClear in a composed layout when the field should be resettable. Children replace the default close icon.
import {
RuiSelect,
RuiSelectClear,
RuiSelectControl,
RuiSelectListbox,
RuiSelectToggle,
RuiSelectTrigger,
RuiSelectValue,
} from '@ecopages/radiant-ui/select';
import { RuiListbox, RuiListboxOption } from '@ecopages/radiant-ui/listbox';
import { RuiIconX } from '@ecopages/radiant-ui/icons';
<RuiSelect value="cat" placeholder="Select an animal">
<RuiSelectControl>
<RuiSelectTrigger>
<RuiSelectValue />
</RuiSelectTrigger>
<RuiSelectClear aria-label="Clear animal selection">
<RuiIconX />
</RuiSelectClear>
<RuiSelectToggle />
</RuiSelectControl>
<RuiSelectListbox>
<RuiListbox embedded>
<RuiListboxOption value="cat">Cat</RuiListboxOption>
<RuiListboxOption value="dog">Dog</RuiListboxOption>
</RuiListbox>
</RuiSelectListbox>
</RuiSelect>Always set embedded on a listbox inside the popup so the select owns selection and keyboard.
Custom markup
<rui-select> coordinates any light-DOM tree that matches its query contract. The Rui* helpers stamp these targets; they are not required.
import '@ecopages/radiant-ui/select';
import '@ecopages/radiant-ui/listbox';
<rui-select value="cat" placeholder="Select an animal">
<div data-ref="root" class="rui-select">
<div class="rui-select__control">
<div data-select-trigger tabindex="0">
<span data-select-value></span>
</div>
<button type="button" data-select-toggle></button>
</div>
<div data-select-listbox hidden>
<rui-listbox embedded>
<div role="option" data-value="cat">Cat</div>
<div role="option" data-value="dog">Dog</div>
</rui-listbox>
</div>
</div>
</rui-select>BEM classes are presentation-only. Nested rui-listbox must have embedded. For multi-select chips, place rui-tag-group inside [data-select-value].
Multiple selection
Set selectionMode="multiple". The default options composition shows selected values as removable tags and adds a check indicator on each option. Composed layouts place a RuiTagGroup inside RuiSelectValue, and RuiListboxOptionIndicator inside an option to replace the check.
<RuiSelect
selectionMode="multiple"
value={['cat', 'dog']}
placeholder="Select animals"
options={[
{ value: 'cat', label: 'Cat' },
{ value: 'dog', label: 'Dog' },
]}
/>The popup stays open while options are toggled. Pass shouldCloseOnSelect to close after each choice.
Searchable lists
Wrap the embedded listbox in RuiAutocomplete and add RuiSelectSearch when the collection is long. Opening the popup moves focus into the search field.
Trigger kind
triggerKind controls what opens the listbox (same attribute name as RuiCombobox; select supports only focus and manual):
| Value | Opens when |
|---|---|
manual (default) | Click, toggle, or arrow keys on the trigger |
focus | The trigger receives focus (plus click, toggle, and arrow keys) |
Theming
Select surfaces map to semantic surface roles, never Tailwind palette steps. Override at the theme layer, not in component CSS.
| Part | CSS roles |
|---|---|
Trigger (rui-select__trigger) | on-background; border on the control row |
Control row (rui-select__control) | border, background, rounded-control |
Popup (rui-select__listbox) | popover surface + shadow-overlay |
| Selected value / toggle | on-surface |
Geometry shares control tokens (--size-control-*, --radius-control, --space-control-*) with RuiInput and RuiButton.
Customization of icons is through Composition Helper children, not CSS variables: pass children to RuiSelectToggle, RuiSelectClear, and RuiListboxOptionIndicator.
Accessibility
- The trigger exposes
role="combobox",aria-expanded, andaria-haspopup="listbox". - Multiple mode marks the popup listbox
aria-multiselectable="true". RuiSelectClearneeds an accessible name (aria-label="Clear selection"by default). Clicking it clears the value and returns focus to the trigger.- Provide a visible label. The placeholder is not a substitute.
API
RuiSelect is a custom element (<rui-select>) wrapping a button trigger and an embedded RuiListbox. It is not form-associated.
Attributes (<rui-select>)
| 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 when there is no associated RuiLabel. |
placeholder | string | '' | Shown when nothing is selected. |
disabled | boolean | false | Disabled state. |
selection-mode | single · multiple | single | Single or multi-select. |
should-close-on-select | boolean | Whether selecting closes the popup (defaults to true for single, false for multiple). | |
trigger-kind | focus · manual | manual | Controls what opens the listbox. |
Light-DOM contract
| Target | Required | Host writes | Author owns |
|---|---|---|---|
[data-ref="root"] | yes | — | shell for popover anchoring |
[data-select-trigger] | yes | role, aria-expanded, aria-controls, aria-haspopup, id, tabIndex, aria-disabled | combobox div (not a native <button> when chips with remove controls are inside) |
[data-select-value] | yes | data-placeholder, optional [data-select-placeholder] | content / rui-tag-group |
[data-select-listbox] | yes | hidden | popup shell |
[role="option"] | yes (in listbox) | aria-selected, option id | data-value, label text |
[data-select-toggle] | no | aria-expanded, disabled | the button |
[data-select-clear] | no | hidden, disabled | the button |
[data-autocomplete-input] | no | role, aria-* when popup open | search field |
Do not set role, aria-expanded, or tabIndex on the trigger. Nested hosts: rui-listbox (embedded), rui-tag-group in [data-select-value], optional rui-autocomplete in the listbox.
Events
| Event | Detail | Description |
|---|---|---|
rui-change | { value: string[] } | Emitted after a selection changes. |
View helpers
| Component | Target stamped | Notes |
|---|---|---|
RuiSelect | <rui-select> + [data-ref="root"] | Accepts options or children |
RuiSelectControl | — | Trigger row wrapper |
RuiSelectTrigger | [data-select-trigger] | Combobox div; place RuiSelectValue inside |
RuiSelectValue | [data-select-value] | Tags via RuiTagGroup in multiple mode |
RuiSelectClear | [data-select-clear] | Hidden when empty |
RuiSelectToggle | [data-select-toggle] | Children replace chevron |
RuiSelectListbox | [data-select-listbox] | Place embedded RuiListbox inside |
RuiSelectSearch | [data-autocomplete-input] | Inside RuiAutocomplete |
CSS classes
| Class | Description |
|---|---|
.rui-select__control | Trigger row: bordered control-height surface. |
.rui-select__trigger | Combobox surface (role="combobox"). |
.rui-select__value | Selected value / placeholder text. |
.rui-select__listbox | Popup shell (adds rui-popover rui-popover--listbox rui-floating). |
.rui-select__search | Filtering input inside the popup. |
Theme roles
| Part | CSS variables consumed |
|---|---|
| Control row | --border, --background, --radius-control |
| Trigger text | --on-background |
| Value / toggle | --on-surface |
| Popup | --shadow-overlay (via rui-popover) |
| Geometry | --size-control-*, --space-control-* |