<bmx-select>
A list of options behind a closed control: one choice or several, grouped or flat, and windowed so that ten thousand of them cost the same as ten.
27 properties · 5 events · 8 methods · 14 parts
Example
Show markup
<div class="row">
<bmx-select id="ex-select-country" label="Country" placeholder="Choose a country" description="Type a letter with the list closed and it jumps straight there, as a native select does."></bmx-select>
</div>
<div class="row">
<bmx-select id="ex-select-team" label="Assign to" multiple="true" clearable="true" max-tags="2" description="Several choices, collapsed into a count once there are more than two."></bmx-select>
</div>
<div class="row">
<bmx-select id="ex-select-big" label="Reference" placeholder="One of five thousand" description="Windowed: about twenty rows exist at a time, whatever the scrollbar says."></bmx-select>
</div>
<script type="module">
// `options` is a property rather than an attribute, because it is an array.
// Waiting for the element to be defined means the assignment cannot land on
// a plain <bmx-select> that has not upgraded yet.
await customElements.whenDefined('bmx-select');
document.getElementById('ex-select-country').options = [
{ value: 'gb', label: 'United Kingdom', group: 'Europe' },
{ value: 'ie', label: 'Ireland', group: 'Europe' },
{ value: 'de', label: 'Germany', group: 'Europe' },
{ value: 'us', label: 'United States', group: 'Americas' },
{ value: 'ca', label: 'Canada', group: 'Americas' },
{ value: 'br', label: 'Brazil', group: 'Americas', disabled: true },
];
document.getElementById('ex-select-team').options = [
{ value: 'ana', label: 'Ana Silva', description: 'Platform' },
{ value: 'ben', label: 'Ben Okafor', description: 'Platform' },
{ value: 'cara', label: 'Cara Ellis', description: 'Design' },
{ value: 'dev', label: 'Dev Patel', description: 'Design' },
];
document.getElementById('ex-select-big').options = Array.from({ length: 5000 }, (_, index) => ({
value: `REF-${index}`,
label: `REF-${String(index).padStart(5, '0')}`,
}));
</script>
WHAT IT IS NOT
It is not searchable, and that is a decision rather than an omission. A
control the user can type into is a combobox: its trigger is a text field,
its keyboard is filtering rather than typeahead, and its ARIA is a different
shape. bmx-combobox is that component, and a combobox that refuses values
outside its list is the "searchable select" this one is sometimes asked to
be. Folding both into one tag behind a flag would mean a property that
silently changes an element's role, which is the thing this library keeps
declining to do.
What it does have is typeahead, which is what a native <select> has:
type a few characters and the active option jumps, and repeat one character
to cycle through everything starting with it. That second half is in
src/core/listbox.ts with the rest of the arithmetic.
WHY OPTIONS ARE DATA
options is an array, because a virtualised list has to own its data - a
thousand slotted elements are the exact cost windowing exists to avoid, paid
before the component starts. <bmx-option> children are supported as a
declarative front door for hand-written markup, and are read once into the
same array. A non-empty options property wins; the two are never merged,
because two sources of truth for a list is how a select ends up showing
something the application does not think is there.
VIRTUALISATION, AND WHEN IT TURNS ON
Below virtualThreshold rows every row is rendered and each is whatever
height its content needs. Above it the list is windowed, which requires rows
of a uniform height - so a long list gets one line per option whatever the
label. Short lists, which is nearly all of them, pay nothing for any of this
and are free to have two-line options.
The height is measured off a rendered row rather than read from the custom
property that sets it. getComputedStyle hands a custom property back as the
token that was written - 2.25rem, not 36px - and a virtualiser told its
rows are two pixels tall renders the entire list, which is exactly what
happened the first time this was built.
The keyboard has to be windowing-aware too. scrollIntoView needs an
element, and the row an arrow key just moved to may not be rendered - so the
scroll position is computed from the row offsets instead, in
scrollOffsetFor.
ACCESSIBILITY
The APG select-only combobox pattern: role="combobox" on the trigger with
aria-expanded and aria-controls, a role="listbox" surface, and
aria-activedescendant moving the visual focus while real focus stays on
the trigger. Arrow keys move the active option and Enter commits it - not
the native <select>'s "arrowing changes the value", which cannot be
expressed in this pattern and is not what a screen reader user expects from
something announcing itself as a combobox.
Groups survive windowing: each run of consecutive same-group rows in the
window is wrapped in a role="group", and a window that begins in the middle
of a group is still wrapped in it, using the heading it scrolled past. Every
option carries aria-setsize and aria-posinset counted over the whole
list, because the browser can only count what is rendered.
Properties
| Property | Attribute | Type | Default | Description |
|---|---|---|---|---|
appearance |
appearance |
BmxSelectAppearance |
'outline' |
Visual treatment of the closed control. |
autoFocus |
auto-focus |
boolean |
false |
Focus the control once it has rendered. |
clearable |
clearable |
boolean |
false |
Show a clear button once something is chosen. |
closeOnSelect |
close-on-select |
boolean |
— | Close the list after a choice. Defaults to true for single, false for multiple. |
description |
description |
string |
— | Help text below the field. |
disabled |
disabled |
boolean |
false |
Disable the control. |
emptyText |
empty-text |
string |
'No options' |
What the list says when it has nothing in it. |
errorText |
error-text |
string |
— | An error supplied by the consumer - a server response, typically. |
fullWidth |
full-width |
boolean |
false |
Stretch to the width of the container. |
hideLabel |
hide-label |
boolean |
false |
Hide the label visually while keeping it as the accessible name. |
label |
label |
string |
— | The field's label. Required unless the label slot is used. |
maxTags |
max-tags |
number |
3 |
How many tags to show before the rest collapse into a count. A closed control that grows to four lines because somebody chose eight things pushes the rest of the form down the page every time it is used. |
maxVisibleRows |
max-visible-rows |
number |
8 |
How many rows the open list shows before it scrolls. |
messages |
property only | BmxFieldMessages |
— | Replacements for the default wording, by reason. Accepts the JSON spelling of the object as well, for templates that can only write attributes. See src/core/markup.ts. |
multiple |
multiple |
boolean |
false |
Allow more than one choice. Changes what the closed control shows to tags. |
name |
name |
string |
— | The field's name in the form it belongs to. |
options |
property only | BmxListboxOption[] |
[] |
The options. A property, because it is an array. See the note on the class. Accepts the JSON spelling of the list as well, because an attribute is the only channel some templates have. See src/core/markup.ts. |
placeholder |
placeholder |
string |
'Select…' |
Text shown when nothing is chosen. Never a substitute for a label. |
placement |
placement |
BmxPlacement |
'bottom-start' |
Which side the list opens on when there is room. |
required |
required |
boolean |
false |
Require a choice before the form will submit. |
shape |
shape |
BmxShape |
'rounded' |
Corner treatment. circle is not meaningful here and behaves as pill. |
size |
size |
BmxSize |
'md' |
Size step. |
tone |
tone |
BmxTone |
'primary' |
Semantic colour role, used for the focus ring and the chosen option. |
validateOn |
validate-on |
BmxValidateOn |
'blur' |
When the field is willing to reveal a problem. |
value |
value |
string | null |
null |
The chosen value in single mode. Two-way: the component writes back to it. |
values |
property only | string[] |
[] |
The chosen values in multiple mode. Two-way. Accepts a comma-separated or JSON string as well, because an attribute is the only channel some templates have. See src/core/markup.ts. |
virtualThreshold |
virtual-threshold |
number |
100 |
The row count above which the list is windowed. Below it, rows are rendered in full and may be any height. Above it, the two height custom properties decide the geometry - see the note on the class. Set it to 0 to always window, or to Infinity never to. |
Events
| Event | Detail | Description |
|---|---|---|
bmxBlur |
void |
Fired when the control loses focus. |
bmxChange |
BmxSelectChangeDetail |
Fired when the selection changes. |
bmxFocus |
void |
Fired when the control gains focus. |
bmxOpenChange |
boolean |
Fired when the list opens or closes. |
bmxValidityChange |
BmxSelectValidityDetail |
Fired whenever the resolved validity changes. |
Methods
| Method | Signature | Description |
|---|---|---|
checkValidity |
checkValidity() => Promise<boolean> |
Validate now and return whether the field passed, without revealing anything. |
clear |
clear() => Promise<void> |
Empty the selection. |
closeList |
closeList() => Promise<void> |
Close the list. Focus returns to the control. |
getSelectedOptions |
getSelectedOptions() => Promise<BmxListboxOption[]> |
The options behind the current selection, in the list's order. |
openList |
openList() => Promise<void> |
Open the list. |
removeFocus |
removeFocus() => Promise<void> |
Remove focus from the control. |
reportValidity |
reportValidity() => Promise<boolean> |
Validate, reveal any problem, and focus the field if it has one. |
setFocus |
setFocus(options?: FocusOptions) => Promise<void> |
Focus the control. |
Slots
| Slot | Description |
|---|---|
(default) |
bmx-option elements, when options are written rather than passed. |
description |
Rich help text, in place of the description property. |
label |
Rich label content, in place of the label property. |
CSS shadow parts
| Part | Description |
|---|---|
chevron |
The disclosure arrow. |
clear |
The clear button. |
control |
The trigger carrying role="combobox". |
description |
The help text. |
empty |
The message shown when there are no options. |
error |
The error message. |
field |
The bordered box that is the closed control. |
group |
A group heading. |
label |
The label element. |
listbox |
The floating surface. |
option |
One option row. |
tag |
One chosen value in multiple mode. |
tag-remove |
A tag's remove button. |
value |
The chosen label, or the placeholder. |
CSS custom properties
| Property | Description |
|---|---|
--bmx-select-background |
The control's background. Set by appearance. |
--bmx-select-border-color |
The control's border colour at rest. |
--bmx-select-border-width |
Border width of the control. |
--bmx-select-font-size |
The value's font size. |
--bmx-select-gap |
Space between the value, the tags and the chevron. |
--bmx-select-group-height |
A group heading's height. Load-bearing in the same way. |
--bmx-select-height |
The closed control's height. |
--bmx-select-label-font-size |
The label's font size. |
--bmx-select-list-background |
The floating list's background. |
--bmx-select-list-shadow |
The list's shadow, which is what lifts it off the page. |
--bmx-select-option-active-background |
The background of the option the keyboard is on. |
--bmx-select-option-height |
A row's height. Load-bearing above the virtualisation threshold — see the note above. |
--bmx-select-option-selected-background |
The chosen option's background. |
--bmx-select-padding-inline |
Horizontal padding inside the control. |
--bmx-select-placeholder-color |
Placeholder colour. Dimmer than a value, and still AA against the control. |
--bmx-select-radius |
Corner radius of the control and the list. |
--bmx-select-stack-gap |
Space between the label, the control and the supporting text. |
--bmx-select-support-font-size |
Font size of the description and error message. |