0.1.0

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

Cat
Dog

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.

Cat
Dog
<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.

News
Travel
Shopping
Business
Food
No results.

Trigger kind

triggerKind controls what opens the listbox (same attribute name as RuiCombobox; select supports only focus and manual):

ValueOpens when
manual (default)Click, toggle, or arrow keys on the trigger
focusThe trigger receives focus (plus click, toggle, and arrow keys)
Cat
Dog

Theming

Select surfaces map to semantic surface roles, never Tailwind palette steps. Override at the theme layer, not in component CSS.

PartCSS 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 / toggleon-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, and aria-haspopup="listbox".
  • Multiple mode marks the popup listbox aria-multiselectable="true".
  • RuiSelectClear needs 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>)

AttributeTypeDefaultDescription
valuestringomittedComma-separated selected tokens. Empty selection removes the attribute. The JS property is string[].
labelstring''Accessible name when there is no associated RuiLabel.
placeholderstring''Shown when nothing is selected.
disabledbooleanfalseDisabled state.
selection-modesingle · multiplesingleSingle or multi-select.
should-close-on-selectbooleanWhether selecting closes the popup (defaults to true for single, false for multiple).
trigger-kindfocus · manualmanualControls what opens the listbox.

Light-DOM contract

TargetRequiredHost writesAuthor owns
[data-ref="root"]yes—shell for popover anchoring
[data-select-trigger]yesrole, aria-expanded, aria-controls, aria-haspopup, id, tabIndex, aria-disabledcombobox div (not a native <button> when chips with remove controls are inside)
[data-select-value]yesdata-placeholder, optional [data-select-placeholder]content / rui-tag-group
[data-select-listbox]yeshiddenpopup shell
[role="option"]yes (in listbox)aria-selected, option iddata-value, label text
[data-select-toggle]noaria-expanded, disabledthe button
[data-select-clear]nohidden, disabledthe button
[data-autocomplete-input]norole, aria-* when popup opensearch 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

EventDetailDescription
rui-change{ value: string[] }Emitted after a selection changes.

View helpers

ComponentTarget stampedNotes
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

ClassDescription
.rui-select__controlTrigger row: bordered control-height surface.
.rui-select__triggerCombobox surface (role="combobox").
.rui-select__valueSelected value / placeholder text.
.rui-select__listboxPopup shell (adds rui-popover rui-popover--listbox rui-floating).
.rui-select__searchFiltering input inside the popup.

Theme roles

PartCSS 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-*