Skip to main content

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

Installation​

npm install @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.

KeyAction
Down ArrowOpens the popup on the first option, or moves to the next option.
Up ArrowOpens the popup on the last option, or moves to the previous option.
Home, EndMoves to the first or last option while the popup is open.
Enter, SpaceOpens the popup, or selects the active option. A single select then closes; a multiple select stays open.
EscapeCloses the popup.
TabCloses the popup and moves focus on.
Printable charactersMoves 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:

  1. 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, required with nothing selected, or setCustomValidity()), the component cancels the submission, fires a cancelable invalid event on the select, and moves focus to the trigger. Call preventDefault() on the invalid event to supply your own error UI instead. novalidate forms, formnovalidate submitters, and form.submit() bypass enforcement, matching native scoping.
  2. 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 the disabled attribute present, so re-setting it is indistinguishable from the guard's own writes. Set disabled on <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​

PropertyMarkupTypeDefaultDescription
messagesJS property onlyPartial<RCSelectMessages> | undefinedNot specifiedBuilt-in text overrides; unset keys fall back to registered text, then English.
openopenbooleanfalseReflects whether the popup listbox is open.
multiplemultiplebooleanfalseEnables selection of multiple options simultaneously.
disableddisabledbooleanfalseDisables the trigger and prevents the popup from opening.
requiredrequiredbooleanfalseMirrors 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.
placeholderplaceholderstring''Text shown in the trigger when no value is selected.
displaydisplay'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.
popupModepopup-modeRCSelectPopupMode'popover'Presents options in an anchored popover or transactional modal dialog.
dialogCancelButtondialog-cancel-button'visible' | 'hidden''visible'Whether the leading dialog cancel action is visible.
valueJS property onlyRCSelectValueNot specifiedCurrent selection. Host writes update selection silently; user interaction emits `rc-select-change`.
defaultValueJS property onlyRCSelectValue | undefinedNot specifiedInitial uncontrolled selection, applied before user or native state owns the value.
optionsJS property onlyListboxOption[] | undefinedNot specifiedProgrammatic option source. Omit to derive options from the slotted `<select>`.
selectedValuesJS property onlystring[]Not specifiedSelected values as a consistently-array-shaped convenience getter.

Methods​

MethodDescription
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​

EventDetail typeDescription
rc-select-openCustomEventWhen the popup opens.
rc-select-closeCustomEventWhen the popup closes.
rc-select-changeCustomEventWhen user interaction changes selection. `detail: { value, selectedValues, selectedOptions }`

Slots​

NameDescription
(default)Required. A native `<select>` element used for form submission and as the source of truth for options, multiple, and disabled state.
displayOptional. Replaces the default value label in the trigger.
toggle-indicatorOptional. 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​

PropertyDefaultDescription
--rc-select-max-height20emMaximum popup height.
--rc-select-control-block-sizevar(--rc-control-block-size)Trigger block size.
--rc-select-padding-blockvar(--rc-control-padding-block)Trigger block-axis padding.
--rc-select-padding-inlinevar(--rc-control-padding-inline)Trigger inline-axis padding.
--rc-select-gapvar(--rc-control-gap)Gap between trigger content, chips, and icon.
--rc-select-radiusvar(--rc-control-radius)Trigger border radius.
--rc-select-bordervar(--rc-border)Trigger border.
--rc-select-listbox-radiusvar(--rc-popup-radius)Popup listbox border radius.
--rc-select-listbox-duration150msPopup listbox open/close fade transition duration.
--rc-select-listbox-bordervar(--rc-popup-border)Popup listbox border.
--rc-select-listbox-backgroundvar(--rc-popup-background)Popup listbox background.
--rc-select-listbox-colorvar(--rc-popup-color)Popup listbox text color.
--rc-select-shadowvar(--rc-popup-shadow)Popup listbox box shadow.
--rc-select-listbox-padding-blockvar(--rc-popup-padding-block)Popup listbox block padding.
--rc-select-chip-radiusvar(--rc-radius-md)Multi-select chip border radius.
--rc-select-chip-padding-block0.1emMulti-select chip block-axis padding.
--rc-select-chip-padding-inline-start0.3emMulti-select chip leading padding.
--rc-select-chip-padding-inline-endcalc(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-gapcalc(var(--rc-control-gap, 0.25em) * 0.8)Gap between chip content items.
--rc-select-chip-bordervar(--rc-border)Multi-select chip border.
--rc-chip-remove-target-size1.5remInherited remove-target width reserved by generated multi-select chips.
--rc-chip-remove-offset-inline0.125remInherited edge offset reserved by generated multi-select chips.
--rc-chip-touch-target-overlap-block-start0pxSpace returned between wrapped chip rows after a touch target overlaps the field's leading padding.
--rc-chip-touch-target-overlap-block-end0pxSpace returned between wrapped chip rows after a touch target overlaps the field's trailing padding.
--rc-select-toggle-indicator-size1.1emInline size of the toggle indicator container.
--rc-select-dialog-gap1remGap between fullscreen dialog regions.
--rc-select-dialog-header-gap0.5remGap between fullscreen dialog header slots.

CSS Parts​

PartDescription
anchorThe flex container wrapping chips and trigger.
triggerThe combobox trigger element (div).
chipsThe chips group container (when multiple).
chipIndividual chip (when multiple).
chip-labelText label inside a chip.
chip-removeRemove button inside a chip.
value-displayThe text label showing selected value(s).
toggle-indicatorThe open/close indicator container.
listboxThe `<rc-listbox>` popup element.
dialogDialog popup surface.
dialog-headerManaged leading/title/trailing header.
dialog-cancelLeading button that discards pending changes.
dialog-cancel-iconDecorative close icon inside the cancel button.
dialog-titleVisible dialog title.
dialog-selectedScrollable pending-selection chip region.
dialog-actionsDialog confirmation action group.
dialog-confirmCommits pending changes and closes the dialog.
dialog-listboxDialog option list.