bwc-select
A single-value select with a floating listbox, built on the shared floating
core (src/floating). The trigger shows the selected label; options live in
the popup.
import "basic-web-components/select";Usage
Uncontrolled with a placeholder:
<bwc-select placeholder="Pick a fruit">
<button slot="trigger"><span data-value></span></button>
<div slot="popup">
<div data-option data-value="apple">Apple</div>
<div data-option data-value="banana" data-label="Banana!">Banana</div>
</div>
</bwc-select>Controlled with form participation:
<bwc-select id="s" name="fruit" value="apple">
<button slot="trigger"><span data-value></span> <span data-icon>▾</span></button>
<div slot="popup">
<div data-option data-value="apple">Apple</div>
<div data-option data-value="banana">Banana</div>
</div>
</bwc-select>
<script>
const s = document.getElementById("s");
s.onValueChange = (value) => s.setAttribute("value", value);
</script>Trigger clicks toggle; selecting an option sets the value and closes (in
multiple mode options toggle instead and the popup stays open).
API
| Property | Attribute | Type | Default | Notes |
|---|---|---|---|---|
open |
open |
boolean |
false |
Present (or assigned) → controlled mode. Reflects to open. |
defaultOpen |
default-open |
boolean |
false |
Seed for uncontrolled mode. |
value |
value |
string |
"" |
Present (or assigned) → controlled mode. |
defaultValue |
default-value |
string |
"" |
Seed for uncontrolled mode. |
values |
values |
string[] (JSON array attribute) |
[] |
Multi-select values; present (or assigned) → controlled mode. |
defaultValues |
default-values |
string[] (JSON array attribute) |
[] |
Seed for uncontrolled multi-select. |
multiple |
multiple |
boolean |
false |
Options toggle without closing; display joins labels with ", ". |
name |
name |
string |
"" |
Reflected into hidden input(s) for native form participation. |
form |
form |
string |
"" |
Passed to hidden input(s) to associate with a form by id. |
required |
required |
boolean |
false |
Forwarded to the hidden input(s). |
readonly |
readonly |
boolean |
false |
Opens, but selection changes are ignored. |
disabled |
disabled |
boolean |
false |
Disables the trigger; open/selection requests are ignored. |
modal |
modal |
boolean |
true |
Locks body scroll while open; light-dismiss always applies. |
loopFocus |
loop-focus |
boolean |
true |
Wrap highlight past the first/last option. |
placeholder |
placeholder |
string |
"" |
Display text when no value is selected. |
matchTriggerWidth |
match-trigger-width |
boolean |
true |
Size the popup to at least the trigger width while open. |
side |
side |
"top" | "right" | "bottom" | "left" |
"bottom" |
Preferred side; flips when it overflows. Invalid values throw. |
align |
align |
"start" | "center" | "end" |
"center" |
Cross-axis alignment. Invalid values throw. |
sideOffset |
side-offset |
number |
0 |
Gap in px. Non-finite values throw. |
alignOffset |
align-offset |
number |
0 |
Cross-axis shift in px. Non-finite values throw. |
strategy |
strategy |
"absolute" | "fixed" |
"fixed" |
Positioning strategy. Invalid values throw. |
anchor |
anchor |
Element | string | null |
null |
Override anchor (element or selector); null anchors to trigger. |
triggerClass |
trigger-class |
string |
"" |
Extra classes for the trigger. |
popupClass |
popup-class |
string |
"" |
Extra classes for the popup. |
onOpenChange |
— (property only) | ((open: boolean) => void) | null |
null |
Must be a function or null. |
onValueChange |
— (property only) | ((value: string) => void) | null |
null |
Must be a function or null. |
onValuesChange |
— (property only) | ((values: string[]) => void) | null |
null |
Must be a function or null. |
Methods
| Method | Notes |
|---|---|
show() |
Imperative open (respects disabled). |
close() |
Imperative close. |
toggle(force?) |
Toggle, or force with a boolean. |
selectValue(v) |
Select a value (ignored when readonly/disabled). |
selectValues(vs) |
Replace all values in multiple mode (same guards). |
clear() |
Clear the value(s) (ignored when readonly/disabled). |
open is the boolean state property, so imperative open is show() —
same split as HTMLDialogElement (open + show()/close()).
Events
| Name | Detail | Bubbles / composed |
|---|---|---|
open-change |
{ open: boolean } |
yes / yes |
value-change |
{ value: string } |
yes / yes |
values-change |
{ values: string[] } |
yes / yes |
Slots
| Slot | Required element |
|---|---|
trigger |
exactly one button (containing [data-value], optional [data-icon]) |
popup |
exactly one div |
Options are [data-option][data-value] elements inside the popup, with an
optional data-label display override. Disabled options carry
[data-disabled] (or disabled / aria-disabled="true") and are skipped
by keyboard navigation. Wrong slot count or type throws TypeError. The
popup gets role="listbox" and data-testid="bwc-select-popup"; options
get role="option" (when absent), aria-selected, and data-selected;
parts keep author classes plus select-trigger / select-popup marker
classes and stable testids.
- Trigger gets
aria-haspopup="listbox",aria-controls→ popup id, andaria-expanded; state mirrors asdata-open/data-closed/data-disabled/data-modal/data-multipleon trigger and popup, plusdata-placeholderon the trigger while the placeholder shows. Disabled trigger uses thenot-allowedcursor. - Keyboard:
ArrowDown/ArrowUp,Home/End,Enter/Spaceselects the highlighted option and closes,Escapecloses and returns focus to the trigger. Printable characters run label typeahead over enabled options — from the popup it highlights, from a closed trigger it opens then highlights;Enterstill selects. Highlight mirrors asdata-highlightedwith rovingtabindex. - In
multiplemode, clicks andEnter/Spacetoggle options without closing and firevalues-change;valuemirrors the first selection. [data-scroll-up]/[data-scroll-down]buttons in the popup scroll the[data-list]container (or the popup) by a page step and disable themselves at the scrolled edge.- An optional
[data-backdrop]child of the popup mirrorsdata-open/data-closedandhiddenfor author dim overlays. Whilemodalis open, body scroll locks (light-dismiss still applies). - Enter/exit hooks: the popup takes
data-starting-styleon open (removed next frame) anddata-ending-styleon close while a CSS transition/animation is present (hide defers until it finishes); without CSS the popup hides synchronously. - Positioning runs through the shared floating core (
src/floating): JS measures the anchor and writesleft/topplusdata-side(placed side, after flip) /data-align; scroll/resize/layout changes reposition while open. A[data-arrow]child of the popup is centered on the cross axis automatically.match-trigger-widthalso sets the popupmin-widthto the trigger width while open.
Form behavior
With name set, hidden input(s) carry the value so the select
participates in native form submission: one input in single mode, one per
value in multiple mode (nothing submits when empty). form associates
the input(s) with a form by id; required and disabled forward.
Errors
TypeErrorfor invalid slot topology, invalidside, non-finite offsets, non-stringvalues, or non-function callbacks.
State ownership
open, value, and values are independently uncontrolled until assigned;
their default-* properties seed internal state. Assigning one permanently
controls it. Property assignments and imperative methods apply immediately
and emit the matching change event when effective state changes; other
interactions only request changes for controlled state.