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.
| Variable | Use |
|---|---|
--select-radius | Corner radius; the sm and lg variants reassign this |
--select-height | Single-line height; the sm and lg variants reassign this |
--select-padding-inline | Horizontal padding; the sm and lg variants reassign this |
--select-padding-block | Vertical padding; defaults to 0 (height owns the rhythm) |
--select-font-size | Text size; the sm and lg variants reassign this |
--select-indicator | Chevron SVG; a data: URL with a literal stroke per theme (the dark scope swaps to a lighter chevron) |
--select-bg | Background; readonly shifts a tier |
--select-color | Text color |
--select-border-width | Border thickness |
--select-border-color | Border color; validation hooks flip this to the danger token |
--select-shadow | Custom popup elevation |
--select-popup-border-color | Popup rim (defaults a tier lighter than the trigger border) |
--select-item-min-height / -item-padding-block / -item-padding-inline | Option row layout |
--select-item-bg-hover / -item-color-hover | Highlighted row paint (mouse hover and keyboard cursor) |
--select-item-bg-active / -item-color-active | Selected row paint |
--select-group-label-color | Optgroup label color inside the popup |