Select

A select styled to match the input — native by default, or a styled popup on demand.

Basic

Add .select to a <select> for the no-JS baseline: the input field shape plus a themed chevron in the inline-end padding well, shared with .input and .textarea. Pair with a label via for/id. Add data-stisla-select and the behavior layer hides the native control and renders a styled trigger and listbox popup (type-to-jump, a single-line multi summary, keyboard nav) while the original <select> stays in the DOM so form submission and validation work as authored. The two halves of this page cover each surface in turn. For typing-ahead search see Combobox; for a free-form input that suggests options see Autocomplete.

Sizes

Three sizes match the input scale. Add .select--sm or .select--lg.

Option groups

Wrap related options in <optgroup> to label sections in the native dropdown.

Multiple

A multiple (or size > 1) select opts out of the single-line shape: it drops the chevron and renders as a native inline list, the same as a textarea grows.

Size attribute

A size greater than 1 renders a fixed-height scrolling list (no chevron), the same multi-line shape as multiple.

Helper text

Pair a .field__description with the select and wire it via aria-describedby so assistive tech announces the hint.

Disabled

Add disabled to block interaction and dim the field.

Browser validation

Pair required with a placeholder <option value=""> and the form blocks submit until a real value is picked, painting the native :user-invalid state. Pair with a .field__error.

Server validation

Set aria-invalid="true" from your form library for sticky server errors. The danger border paints; pair with a .field__error.

Custom popup

Add data-stisla-select to a <select class="select"> to hydrate. The native control is hidden (still in the DOM, so form submission and validation work as authored) and a styled trigger plus a listbox popup take over. The trigger reuses the same .select shell, so sizes, density, and validation all carry over. Set data-placeholder for the empty-state label.

Keyboard

The trigger behaves like a native <select>.

  • Space / Enter / ArrowDown: open
  • ArrowUp / ArrowDown: move the highlight (loops at the ends)
  • Home / End: first / last enabled option
  • Enter / Space: select the highlighted option (toggles in multi)
  • Escape: close
  • Tab: close and move on
  • Type a letter to jump to the first option that starts with it (keep typing within 500 ms to extend the buffer)

Multiple

Add the native multiple attribute and the trigger shows the first chosen label plus +N more so the field stays on one line. Click a selected option in the popup to toggle it off.

Option groups

Wrap related options in <optgroup> to label sections inside the popup.

Disabled and invalid

disabled on the source dims the trigger and blocks opening; aria-invalid="true" paints the danger border. Both states mirror from the hidden source onto the trigger, so a form library keeps treating the <select> as the field.

Events

Listen for stisla:select:change to react to selection changes; detail.value is a string for single and an array for multi. The underlying <select> also fires its native change event, so existing form code keeps working.

Customization

These variables retune .select without touching component CSS, under the same field surface as .input / .textarea. Override on the select, a parent scope, or :root. The custom popup adds its own --select-* variables on the shared menu surface (Menu, Combobox, Autocomplete).

On touch devices (@media (pointer: coarse)) --select-font-size is bumped to 1rem so iOS Safari doesn't zoom on focus.

VariableUse
--select-radiusCorner radius; the sm and lg variants reassign this
--select-heightSingle-line height; the sm and lg variants reassign this
--select-padding-inlineHorizontal padding; the sm and lg variants reassign this
--select-padding-blockVertical padding; defaults to 0 (height owns the rhythm)
--select-font-sizeText size; the sm and lg variants reassign this
--select-indicatorChevron SVG; a data: URL with a literal stroke per theme (the dark scope swaps to a lighter chevron)
--select-bgBackground; readonly shifts a tier
--select-colorText color
--select-border-widthBorder thickness
--select-border-colorBorder color; validation hooks flip this to the danger token
--select-shadowCustom popup elevation
--select-popup-border-colorPopup rim (defaults a tier lighter than the trigger border)
--select-item-min-height / -item-padding-block / -item-padding-inlineOption row layout
--select-item-bg-hover / -item-color-hoverHighlighted row paint (mouse hover and keyboard cursor)
--select-item-bg-active / -item-color-activeSelected row paint
--select-group-label-colorOptgroup label color inside the popup