Select
A dropdown selector supporting single/multiple selection, groups, clearable, remote search, and custom creation, with size/status variants, controlled open, and selection limits, fully operable by keyboard.
Single Select
Single select
Accessible Name (label)
label (accessible name)
The label attribute provides the trigger's accessible name (aria-label): when set, screen readers announce it and it takes priority over the placeholder; when unset, it falls back to placeholder. The visible field-label position is handled by oas-form-item, and native label for association (click-to-focus) goes through the form-associated channel.
Preset Value
Preset value
Multiple Select
Multiple
In multiple mode value is a JSON array; selected items are shown as tags that can be removed individually. Tags wrap to new lines by default and the trigger grows with the content (no collapsing unless max-tag-count is set).
Hide Selected (hide-selected)
hide-selected: selected options hidden from the dropdown
hide-selected (multiple mode only) removes selected options from the dropdown — the list only shows "what is still selectable", useful for multi-select over large option sets; clearing (clearable) or removing a tag puts the option back into the list automatically. Selecting everything shows the empty state.
Disabled
Disabled
Size
Size (size)
size supports small / medium (default) / large; control height and font size follow the global size tokens. When not set explicitly, the nearest oas-config-provider size injection applies (global density sync). Use small inside tables or large on filter bars directly.
Variants
variant
variant mirrors the oas-input three-value variants: outlined (default border) / filled (tinted background, border appears on focus/open) / borderless (no border, no background — for plain-text embedding or low-chrome filter bars); invalid values silently fall back to the default. The status semantic color takes precedence over the variant background.
Status
Validation status (status)
status supports success / warning / error, tinting the trigger border and focus ring. error also mirrors aria-invalid="true" onto the host (recognizable by screen readers); setting aria-invalid directly produces the same visual. Pairs well with form validation.
Readonly
Readonly (readonly) vs disabled
readonly and disabled carry different form semantics: readonly is focusable, copyable, never opens the dropdown, and the value still submits (clear and tag-remove affordances are hidden); disabled is not focusable and its value does not submit. Use readonly to display values decided elsewhere, disabled to block interaction.
Empty State
No data
When options are empty, the dropdown shows "No data".
Custom Empty
Custom empty (empty slot)
template[slot="empty"] overrides both the "no data" and "no match" default texts (which come from the locale registry); the content is cloned into the dropdown empty area.
Searchable
Searchable
Once opened, you can type directly to filter; when nothing matches, "No matching options" is shown.
Custom Filtering
Custom filtering (filterMethod)
By default options are matched by label inclusion; assigning el.filterMethod = (query, option) => boolean (a JS property channel — attributes cannot carry functions) takes over local filtering, where query is the raw input and option the full option object. This example (see the page script at the bottom) matches both value and label (pinyin-initial matching and other custom rules work the same way); in remote mode the host owns data-side filtering and filterMethod does not apply.
Controlled Search Input (input-value)
input-value (controlled search text)
input-value makes the search text controllable: when the attribute is present it is the source of truth for the search box (presets/external updates sync into the box and re-filter); user typing writes the attribute back and dispatches oas-input-value-change (detail: { value }) — the host can listen or simply read the attribute.
Reserve Keyword (reserve-keyword)
reserve-keyword (keep the keyword after selection)
In multiple + searchable mode, the keyword is cleared by default after selecting an option (the list resets so you can pick unrelated items); with reserve-keyword the keyword is kept, ideal for picking multiple items among similar results.
Highlight First Option (default-active-first-option)
default-active-first-option
When set, opening the dropdown highlights the first visible option (Enter picks it); by default the currently selected option is highlighted.
Groups
Grouped
Grouped multiple
Options carrying a group field are rendered under a group title (not selectable), with items indented; keyboard ↑/↓ navigates continuously across groups.
Clearable
Clearable
When a value is selected, a clear button appears; clicking clears the value and dispatches oas-clear and oas-change.
Trigger Affixes & Custom Icons
prefix / suffix / suffix-icon / clear-icon slots and show-arrow
template[slot="prefix"] / template[slot="suffix"] render into the trigger affixes (icons, unit text, etc.); template[slot="suffix-icon"] replaces the default dropdown arrow (rotating with the container when expanded); template[slot="clear-icon"] replaces the default × icon of the clear button. show-arrow="false" hides the default arrow (arrow-less search-trigger scenarios).
Remote Search
Remote search (remote + loading)
In remote mode the component does no local filtering: typing dispatches oas-input for the host to request data, and the host sets loading during the request to show a loading placeholder. This example simulates an 800ms delayed filter:
Remote loading placeholder
Loading Text (loading-text)
loading-text (custom loading copy)
loading-text overrides the default loading placeholder copy (built-in locale copy by default); pair it with remote / loading.
Remote Debounce
Remote debounce (debounce)
debounce (milliseconds, default 0 — dispatch immediately, leaving existing behavior untouched) applies only to oas-input dispatching in remote mode: rapid inputs within the window collapse into a single last-value dispatch, sparing the host a setTimeout boilerplate; local filtering is always instant.
Tag Collapse
Tag collapse (max-tag-count)
Multiple-select tags wrap by default and do not collapse; only when max-tag-count is explicitly set do they collapse into +N (hover to see the remaining items).
Hint & Auto Width
hint copy / auto-width trigger
hint renders one line of hint copy below the trigger (wired via aria-describedby, screen-reader reachable); auto-width lets the trigger width shrink to fit the selected item/content (fixed 220px by default).
Reserve Widest Option (reserve-width)
reserve-width + auto-width: no jitter when switching (open the dropdowns and switch to compare)
reserve-width (opt-in) measures the max text width of all option labels plus the placeholder and locks the trigger min-width: the trigger never jitters when switching values. It pairs well with auto-width — after shrinking to content, the reserved value acts as the floor (the first select above is locked to the widest option, the second is the control; open the dropdowns and switch values to see the difference). Both the options attribute and the declarative child channel participate; content changes re-measure automatically; removing the attribute releases the lock. The text width is an approximation (trigger chrome margin tunable via the --oas-select-reserve-pad variable, default 46px).
Max Count
Selection limit (max-count)
With multiple and max-count set, once the selection reaches the limit, unselected options become disabled and grayed out (selected ones can still be deselected, which re-enables the rest); attempts to exceed the limit — click or keyboard — dispatch oas-exceed-limit (detail: { value, max }), which this example uses to update the hint tag. max-count has no effect in single-select mode. Voting and tag-cap scenarios work out of the box.
Allow Create
Allow create
When search yields no match, a "Create {label}" item is shown; clicking or pressing Enter creates a new option from the input value and selects it. Creation dispatches oas-create (detail: { value, label } carrying the created text).
Custom Option Rendering
Custom options (icons + rich text)
Listen to oas-option-render (each option row) and oas-tag-render (multi-select tags); detail.element is the corresponding text container that the host can rewrite into any content (icons, rich text). You can also put <template slot="option"> / <template slot="tag"> inside the component for a static skeleton — [data-option-label] / [data-tag-label] nodes get bound to the option/tag text automatically.
Large Datasets (Virtual Scrolling)
10k options virtual scrolling
With virtual set, only the visible window is rendered (reusing the oas-virtual-list window math; leading/trailing padding carries the scroll height) — 10k options scroll smoothly. item-height tunes the fixed row height (default 36). Keyboard ↑/↓ scrolling follows the highlighted item and aria-activedescendant keeps pointing at a visible row; options with a group field fall back to full rendering.
Dropdown Height
Dropdown height (--oas-select-dropdown-height)
The dropdown max height goes through the CSS variable --oas-select-dropdown-height (default 240px) — override it to adjust, no attribute API involved; virtual scrolling follows it as well (re-open the dropdown to apply). The dropdown width stays aligned with the trigger (the majority default).
Declarative Child-Element Channel
Besides the options JSON, you can declare options declaratively with <oas-option> child elements (matching the native <select><option> mental model: the default slot text is the label; attributes map to the options fields — value / disabled / group group title). The options attribute takes precedence when explicitly set; otherwise child elements are parsed and converge into the same rendering path (virtual scrolling, grouping, search etc. are fully identical). Child additions, removals, attribute and text changes re-render automatically (MutationObserver).
Declarative child elements (native select mental model)
Field Mapping (host .map())
When business field names (name/id/team, etc.) differ from the component contract (label/value), align them with a one-line host-side .map() instead of a field-alias attribute — the JSON serialization boundary of Web Components naturally has the host assemble the data, where field mapping costs nothing:
Business field mapping (.map(), dual channel)
The two channels are equivalent (see the page script at the bottom): MEMBERS.map((m) => ({ label: m.name, value: m.id })) then either el.options = ... (the setter reflects to the attribute) or .map() into <oas-option> child elements appended one by one.
Dropdown Header & Footer
Dropdown header/footer slots
template[slot="header"] renders below the search box and above the options; template[slot="footer"] renders below the options (select-all bars, append buttons, etc.). Note: keyboard ↑/↓ navigation does not enter these areas — interactive content inside must be Tab-focusable (like the checkbox/button here) or modeled as an option; the logic is host-managed.
Controlled Open
Controlled open (open + oas-open-change)
Setting the open attribute takes control of the dropdown (present = open, removed = closed; the property channel el.open = true works too); user gestures (click / Esc / outside click / close-on-select) dispatch oas-open-change (detail: { open }) without forcing the state back — the host decides whether to add or remove the attribute (the controlled-component convention). In this example, picking ① auto-opens ②, and ②'s close event is arbitrated by the host removing the attribute.
Autofocus & Tab Order
autofocus / tabindex
autofocus focuses the trigger automatically after mount (the native autofocus does not pierce the Shadow DOM, so the component forwards it); tabindex is forwarded to the internal trigger to control the tab order (the host attribute is moved upon write, avoiding a double tab stop on host + trigger).
Placement
Placement (placement)
placement supports auto (default — prefer bottom, auto-flip when space runs out) / top (force above) / bottom (force below). Use top for bottom-of-form layouts or long lists that feel more natural opening upward.
Selected Option
Selected option (oas-change carries option)
Besides value, the oas-change detail carries the full option object: option for single select (label/value/group/disabled, null when unmatched) and an options array for multiple select — no more host-side reverse lookup when a form submit needs the complete object.
Focus Events
Focus events (oas-focus / oas-blur)
oas-focus / oas-blur dispatch when the component as a whole gains or loses focus (common for form wiring); internal focus moves (trigger ↔ search box) do not produce spurious events.
Events
Change events
Listen to oas-change; detail.value is a string for single select and an array for multiple:
required
required (native validation chain)
required drives the native validation chain (form-associated): when unfilled, checkValidity() returns false (valueMissing) and native form submission is blocked; it recovers to :valid once filled.
API
oas-select
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
allow-create | Allow creating new options from the input value when nothing matches | boolean | — |
auto-width | Trigger width shrinks to fit the selection | — | — |
autofocus | Auto-focus the trigger after mount | boolean | — |
clearable | Clearable (shows a clear button when a value exists; clearing dispatches oas-clear) | boolean | — |
debounce | Remote search input debounce in ms (default 0 = immediate; only debounces oas-input in remote mode, local filtering stays instant) | number | — |
default-active-first-option | Highlight the first visible option on open (defaults to the selected option) | boolean | — |
disabled | Disabled | boolean | — |
hide-selected | Hide selected options from the dropdown in multiple mode; they return after clearing or deselecting | boolean | — |
hint | Hint line below the trigger, wired via aria-describedby | string | — |
input-value | Controlled search text: typing writes back and emits oas-input-value-change; external updates sync into the search box | string | — |
item-height | Fixed row height (px) when virtual scrolling | string | 36 |
label | The trigger's accessible name (aria-label), taking priority over the placeholder | string | — |
loading | Remote loading placeholder (use with remote) | boolean | — |
loading-text | Custom loading placeholder copy (defaults to locale) | string | — |
max-count | Multi-select limit: at the limit unselected options are disabled-greyed and oas-exceed-limit fires; selected items stay removable; single-select unchanged | number | — |
max-tag-count | Collapse tags beyond this count into +N in multiple mode (opt-in; without it tags wrap instead of collapsing) | number | — |
multiple | Multiple select | boolean | — |
name | Form field name (key for native FormData submission; also the key of same-name multiple entries in multiple mode; the browser skips submission when name is absent) | string | — |
open | Controlled open: attribute present = open, removed = closed; host gestures only fire oas-open-change (no forced write-back) | boolean | false |
options | Options, JSON array [{ label, value, disabled?, group? }] | Option[] | string | [] |
placeholder | Placeholder text | — | — |
placement | Dropdown direction: auto (default, bottom-first with flip) / top / bottom (forced, no flip) | string | auto |
readonly | Read-only: focusable and copyable, no dropdown, value immutable (clear/remove buttons hidden) | boolean | — |
remote | Remote search: no local filtering, typing dispatches oas-input for the host to request | boolean | — |
required | Required marker (drives the native valueMissing validation chain; not passed through to the inner control) | boolean | — |
reserve-keyword | Keep the keyword after selection in searchable mode (cleared by default, matching tree-select) | boolean | — |
reserve-width | Reserve the widest option width on the trigger (opt-in): measures the max text width of all option labels plus the placeholder to lock the trigger min-width so switching values never jitters; pairs well with auto-width (the reserved value acts as the floor after shrinking); re-measured automatically when options/children change; removing the attribute releases the lock | boolean | — |
searchable | Searchable (type to filter after opening the dropdown) | boolean | — |
show-arrow | Set "false" to hide the default dropdown arrow | string | true |
size | Size preset small / medium (default) / large: control height/font/chip height scale | string | medium |
status | Validation status: error / warning / success; error mirrors aria-invalid on the host | string | — |
tabindex | Forwarded to the internal trigger (host attribute removed after delegation to avoid double tab stops) | — | — |
value | Current value (JSON array in multiple mode) | string | string[] | — |
variant | Visual variant: outlined (default) / filled / borderless; invalid values fall back silently; status colors take precedence | string | — |
virtual | Virtual scrolling for large datasets: renders only the visible window (reuses oas-virtual-list); options with a group field fall back to full rendering | boolean | — |
Property (JS property; see each row for attribute reflection)
| Property | Description | Type | Default |
|---|---|---|---|
value | Current value: the selected value string, or an array of selected values when multiple (equivalent to reading the value attribute) | string | string[] | — |
Events
| Event | Description |
|---|---|
oas-blur | Fires when the component loses focus |
oas-change | Selection/clear change, detail: { value } |
oas-clear | Clear button clicked, detail: { value } (value before clearing) |
oas-create | Dispatched when allow-create creates a new option, detail: { value, label } |
oas-exceed-limit | Selection attempt past max-count (click/keyboard/create), detail: { value, max } |
oas-focus | Fires when the component gains focus (trigger↔search inner moves are not reported) |
oas-input | Input in remote mode, detail: { value } (for host requests) |
oas-input-value-change | Dispatched on search-text change, detail: { value } |
oas-open-change | Open state flips, detail: { open } |
oas-option-render | Dispatched for each rendered option row, detail: { index, option, element } (element is the option label container; host can rewrite it into icon/rich text) |
oas-tag-render | Dispatched when a multi-select tag renders, detail: { value, label, element } (element is the tag text container; host can rewrite it) |
Slots
| Name | Description |
|---|---|
template[slot="clear-icon"] | Custom clear-button icon |
template[slot="empty"] | Custom empty state (overrides both "no data" and "no match" defaults) |
template[slot="option"] | Static option row template, cloned into each option label container; [data-option-label] nodes get bound to the option label |
template[slot="prefix"] | Trigger prefix content template |
template[slot="suffix"] | Trigger suffix content template |
template[slot="suffix-icon"] | Custom trailing trigger icon (replaces the default arrow) |
template[slot="tag"] | Static multi-select tag template, cloned into each chip text container; [data-tag-label] nodes get bound to the tag label |
CSS Variables
| CSS Variable | Default |
|---|---|
--oas-button-group-radius | var(--oas-radius-md) |
--oas-glass-blur | none |
--oas-glass-ring | transparent |
--oas-select-dropdown-height | 240px |
--oas-select-reserve | 0px |
oas-option
Attributes
| Attribute | Description | Type | Default |
|---|---|---|---|
disabled | Disable this option (not selectable) | — | — |
group | Group title (optional): options in the same group render a consecutive group title (not selectable), items are indented | — | — |
value | Option value (data-carrier field of the declarative child-element channel) | — | — |
Slots
| Name | Description |
|---|---|
| default | Option label content (default slot text) |
Options carrying a
groupfield are rendered under a group title (not selectable), items are indented; keyboard navigation continues across groups.
Keyboard: Enter / ↓ to open, ↑/↓ to move the highlight (works inside the search box too), Enter to select, Esc to close.