rc-select
Select-only combobox backed by a native <select>, following the WAI-ARIA Combobox pattern.
- Package
@rcarls/rc-select- Element
<rc-select>- Native dependency
- Requires a direct child native select
- State model
- Controlled or uncontrolled selection
- Main events
rc-select-openrc-select-closerc-select-change- Related
- Usage rulesTheme previews
Installation
- npm
- Yarn
- pnpm
npm install @rcarls/rc-webcomponents@next
yarn add @rcarls/rc-webcomponents@next
pnpm add @rcarls/rc-webcomponents@next
import '@rcarls/rc-webcomponents/rc-select/define';
Usage
Wrap a native <select>. It stays in the page as the form value and the no-JavaScript
fallback, and its label names the enhanced trigger.
<label>
Fruit
<rc-select placeholder="Choose fruit">
<select name="fruit">
<option value="">Choose fruit</option>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="cherry" disabled>Cherry</option>
</select>
</rc-select>
</label>
Add multiple to the native select for multiple selection.
Live demo
Accessibility
The trigger has role="combobox" and aria-haspopup="listbox", and the popup is an
rc-listbox with role="listbox". Focus stays on the trigger, which tracks the active option
with aria-activedescendant. The trigger takes its accessible name from the select's label or
aria-label unless it has a label of its own.
| Key | Action |
|---|---|
| Down Arrow | Opens the popup on the first option, or moves to the next option. |
| Up Arrow | Opens the popup on the last option, or moves to the previous option. |
| Home, End | Moves to the first or last option while the popup is open. |
| Enter, Space | Opens the popup, or selects the active option. A single select then closes; a multiple select stays open. |
| Escape | Closes the popup. |
| Tab | Closes the popup and moves focus on. |
| Printable characters | Moves to the first option whose label starts with the typed text; a closed single select selects it. |
In a multiple select, Left Arrow on a closed trigger moves focus into the selected chips (Right Arrow in right-to-left text). The same arrows move between chips, moving past the last chip returns to the trigger, Escape returns to the trigger, and Delete, Backspace, Enter, or Space removes the focused chip.
Dialog popup
Set popup-mode="dialog" for constrained viewports. The component-managed fullscreen dialog
provides a leading close action, title, selected chips, option list, and Done action. Selection
is staged: Done updates the native select and fires one change event, while close, Escape, or an
imperative close discards the draft.
<rc-select popup-mode="dialog" multiple placeholder="Choose categories">
<select name="categories" multiple>
<option value="quick">Quick meal</option>
<option value="vegetarian">Vegetarian</option>
</select>
</rc-select>
Theming
The default demo mode shows the component without a package theme. Use the shared preview controls on this page to compare inherited, light, and dark color schemes or to apply the optional Material theme only inside the demo frames.
Inside a Material rc-field, a row of selected chips grows the field below its floated label, so
the label keeps the same offset as every other field. Add rc-select--dense to render 1.5rem chips that
fit on the field's single input line and keep a populated field at the standard 56px height. The
chips keep their full touch target.
<rc-field>
<label slot="label" for="categories">Categories</label>
<rc-select class="rc-select--dense" data-rc-field-control multiple>
<select id="categories" multiple>
<option selected>Beef</option>
<option selected>Cheap</option>
</select>
</rc-select>
</rc-field>
Known Issues
Firefox for Android: <label>-wrapped multiple selects (countermeasure implemented)
"Geckoview and Fennec open select dropdown on tap of parent label; other browsers do not." Bugzilla 1475723
To workaround this bug, rc-select force-disables the slotted <select multiple> and hooks into the associated form's submit and formdata events to preserve constraint validation and form data participation.
Label activation on a disabled control is a spec-defined no-op, mitigating the issue while maintaining progressive enhancement with minimal light DOM meddling.
However, there are two important caveats to this workaround:
- No native validation bubble. The browser cannot render its validation UI on a hidden control, so when submit-time enforcement finds the select invalid (for example,
requiredwith nothing selected, orsetCustomValidity()), the component cancels the submission, fires a cancelableinvalidevent on the select, and moves focus to the trigger. CallpreventDefault()on theinvalidevent to supply your own error UI instead.novalidateforms,formnovalidatesubmitters, andform.submit()bypass enforcement, matching native scoping. - Disabling through the select is one-way after upgrade. Enabling by removing the select's
disabled(attribute or property) is captured: the component mirrors the intent and keeps the guard in place. But disabling through the select is not captured; the guard already holds thedisabledattribute present, so re-setting it is indistinguishable from the guard's own writes. Setdisabledon<rc-select>itself to disable the control after upgrade.
Localization
Built-in text follows the element's own lang, else <html lang>. Override it with
messages, or register it for every instance under rc-select: options, selectedItems, remove, summary, done, cancel. See
Localization.
API
Properties
| Property | Markup | Type | Default | Description |
|---|---|---|---|---|
messages | JS property only | Partial<RCSelectMessages> | undefined | Not specified | Built-in text overrides; unset keys fall back to registered text, then English. |
open | open | boolean | false | Reflects whether the popup listbox is open. |
multiple | multiple | boolean | false | Enables selection of multiple options simultaneously. |
disabled | disabled | boolean | false | Disables the trigger and prevents the popup from opening. |
required | required | boolean | false | Mirrors the slotted native `<select>`'s own `required`, for `aria-required` on the trigger. Read-only in practice: set `required` on the slotted `<select>` itself, the source of truth for form participation and constraint validation. |
placeholder | placeholder | string | '' | Text shown in the trigger when no value is selected. |
display | display | 'auto' | 'chips' | 'compact' | 'auto' | Controls how selected values appear in the trigger. - `'chips'` — each selected value renders as a removable chip. - `'compact'` — selected values are summarized as "First, +N more". - `'auto'` — uses `'chips'` on pointer devices and `'compact'` on touch. |
popupMode | popup-mode | RCSelectPopupMode | 'popover' | Presents options in an anchored popover or transactional modal dialog. |
dialogCancelButton | dialog-cancel-button | 'visible' | 'hidden' | 'visible' | Whether the leading dialog cancel action is visible. |
value | JS property only | RCSelectValue | Not specified | Current selection. Host writes update selection silently; user interaction emits `rc-select-change`. |
defaultValue | JS property only | RCSelectValue | undefined | Not specified | Initial uncontrolled selection, applied before user or native state owns the value. |
options | JS property only | ListboxOption[] | undefined | Not specified | Programmatic option source. Omit to derive options from the slotted `<select>`. |
selectedValues | JS property only | string[] | Not specified | Selected values as a consistently-array-shaped convenience getter. |
Methods
| Method | Description |
|---|---|
focus(options?: FocusOptions) | Forwards focus to the trigger. The host itself is never in the tab order, so without this override, calling `.focus()` on `<rc-select>` (as `rc-field` does when treating it as a control provider) would be a no-op. |
blur() | Forwards blur to the trigger; see `focus()`. |
openPopup() | Opens the popup listbox if not already open or disabled. |
closePopup(returnFocus: unknown) | Closes the popup listbox. |
Events
| Event | Detail type | Description |
|---|---|---|
rc-select-open | CustomEvent | When the popup opens. |
rc-select-close | CustomEvent | When the popup closes. |
rc-select-change | CustomEvent | When user interaction changes selection. `detail: { value, selectedValues, selectedOptions }` |
Slots
| Name | Description |
|---|---|
(default) | Required. A native `<select>` element used for form submission and as the source of truth for options, multiple, and disabled state. |
display | Optional. Replaces the default value label in the trigger. |
toggle-indicator | Optional. Replaces the default chevron indicator. Accepts any element(s); the container shifts to inline-start in RTL via flex direction. Built-in text (chip names, the dialog actions, the compact summary) follows the element's own `lang`, else `<html lang>`: per element through `messages`, or for every select through `registerMessages()` from `@rcarls/rc-common` under `rc-select`. |
CSS Custom Properties
| Property | Default | Description |
|---|---|---|
--rc-select-max-height | 20em | Maximum popup height. |
--rc-select-control-block-size | var(--rc-control-block-size) | Trigger block size. |
--rc-select-padding-block | var(--rc-control-padding-block) | Trigger block-axis padding. |
--rc-select-padding-inline | var(--rc-control-padding-inline) | Trigger inline-axis padding. |
--rc-select-gap | var(--rc-control-gap) | Gap between trigger content, chips, and icon. |
--rc-select-radius | var(--rc-control-radius) | Trigger border radius. |
--rc-select-border | var(--rc-border) | Trigger border. |
--rc-select-listbox-radius | var(--rc-popup-radius) | Popup listbox border radius. |
--rc-select-listbox-duration | 150ms | Popup listbox open/close fade transition duration. |
--rc-select-listbox-border | var(--rc-popup-border) | Popup listbox border. |
--rc-select-listbox-background | var(--rc-popup-background) | Popup listbox background. |
--rc-select-listbox-color | var(--rc-popup-color) | Popup listbox text color. |
--rc-select-shadow | var(--rc-popup-shadow) | Popup listbox box shadow. |
--rc-select-listbox-padding-block | var(--rc-popup-padding-block) | Popup listbox block padding. |
--rc-select-chip-radius | var(--rc-radius-md) | Multi-select chip border radius. |
--rc-select-chip-padding-block | 0.1em | Multi-select chip block-axis padding. |
--rc-select-chip-padding-inline-start | 0.3em | Multi-select chip leading padding. |
--rc-select-chip-padding-inline-end | calc(var(--rc-chip-remove-target-size, 1.5rem) + var(--rc-chip-remove-offset-inline, 0.125rem)) | Multi-select chip trailing padding, including the remove affordance. |
--rc-select-chip-gap | calc(var(--rc-control-gap, 0.25em) * 0.8) | Gap between chip content items. |
--rc-select-chip-border | var(--rc-border) | Multi-select chip border. |
--rc-chip-remove-target-size | 1.5rem | Inherited remove-target width reserved by generated multi-select chips. |
--rc-chip-remove-offset-inline | 0.125rem | Inherited edge offset reserved by generated multi-select chips. |
--rc-chip-touch-target-overlap-block-start | 0px | Space returned between wrapped chip rows after a touch target overlaps the field's leading padding. |
--rc-chip-touch-target-overlap-block-end | 0px | Space returned between wrapped chip rows after a touch target overlaps the field's trailing padding. |
--rc-select-toggle-indicator-size | 1.1em | Inline size of the toggle indicator container. |
--rc-select-dialog-gap | 1rem | Gap between fullscreen dialog regions. |
--rc-select-dialog-header-gap | 0.5rem | Gap between fullscreen dialog header slots. |
CSS Parts
| Part | Description |
|---|---|
anchor | The flex container wrapping chips and trigger. |
trigger | The combobox trigger element (div). |
chips | The chips group container (when multiple). |
chip | Individual chip (when multiple). |
chip-label | Text label inside a chip. |
chip-remove | Remove button inside a chip. |
value-display | The text label showing selected value(s). |
toggle-indicator | The open/close indicator container. |
listbox | The `<rc-listbox>` popup element. |
dialog | Dialog popup surface. |
dialog-header | Managed leading/title/trailing header. |
dialog-cancel | Leading button that discards pending changes. |
dialog-cancel-icon | Decorative close icon inside the cancel button. |
dialog-title | Visible dialog title. |
dialog-selected | Scrollable pending-selection chip region. |
dialog-actions | Dialog confirmation action group. |
dialog-confirm | Commits pending changes and closes the dialog. |
dialog-listbox | Dialog option list. |