Skip to content

Select

A styled select built on the native customizable-select API, rendered as <select is="root-select">.

Usage

<select is="root-select" data-label="Choose an option">
  <option value="1">Option 1</option>
  <option value="2">Option 2</option>
  <option value="3">Option 3</option>
</select>

Where the customizable-select API is supported, the component injects the <button><selectedcontent></selectedcontent></button> picker structure and styles the picker, options, checkmark and picker icon; options may contain rich markup. Where it is not supported, it leaves a plain native <select> as the fallback.

The component also builds a [field-wrapper] span around the control - always, with or without a label - and that span carries the field box; the <label> and the hint are its siblings.

The arrow is the system's own chevron - the same shape root-tree-view draws on a branch row, masked onto a span so it takes currentcolor and turns over when the picker opens. The user agent's ::picker-icon is hidden either way.

The field's floor is Input's floor: 20ch, twenty characters of the field's own font, which is what a native <input> starts at. It used to measure 4.5px wider than an Input's from the same declaration - ch is twenty characters of the element's font, and the wrapper had inherited the label's medium weight instead of stating the field's own.

Placeholder

A leading <option value="" disabled> reads as prompt text rather than a real choice - shown in the field's placeholder color for as long as it's the current selection, and never itself selectable once left, the same way disabled keeps any option out of reach.

<select is="root-select" data-label="Country">
  <option value="" disabled selected>Choose a country…</option>
  <option value="it">Italy</option>
  <option value="fr">France</option>
</select>

Sizes

data-size takes small and large; medium is the default and needs no value written. The step moves the field's height and the chevron with it - the text stays at the base step at every size, which is the floor under which iOS Safari zooms the viewport on focus.

<select is="root-select" data-size="small" data-label="Small">
  <option>Option 1</option>
</select>
<select is="root-select" data-label="Medium">
  <option>Option 1</option>
</select>
<select is="root-select" data-size="large" data-label="Large">
  <option>Option 1</option>
</select>

Hint and error

data-hint holds the description and data-error the error message: both are authored once, and data-invalid decides which one shows. With no data-error the browser's own validationMessage stands in. One node carries both, wired to the control through aria-describedby.

<select is="root-select" data-label="Country" data-hint="Where you live" data-error="Pick one">
  <option>Italy</option>
</select>

Attributes

Attribute Type Default Description
value string - Selected value
data-size 'small' | 'medium' | 'large' 'medium' Control size
disabled boolean false Disables the select
data-label string - Label text; the component builds the <label>
required boolean false Marks the field required and draws the asterisk
data-hint string - Description shown under the field
data-error string - Error message; falls back to validationMessage
data-invalid 'true' - Shows the error instead of the description

Events

Namespaced as <component>.<event>, bubbling and cancelable; detail also carries the originating DOM event as originalEvent.

Event Detail Description
root-select.input { value } Fired on input
root-select.change { value } Fired on change
root-select.focus { value } Fired on focus
root-select.blur { value } Fired on blur
root-select.invalid { value } Fired when the control fails constraint validation
root-select.data-label.change { value, oldValue } Fired when the label text changes

Extends

HTMLSelectElement - use with <select is="root-select">.