Wrap a native <select> in .field and
give the label a matching for. Select turns it into an
exposed menu, a text field paired with a menu. Add
outlined (or border) on that field for the
outlined variant.
AutoInit() starts every select except those marked
no-autoinit. Add browser-default to keep the native control.
Filled enhanced selects reserve space for their label, including translated labels that wrap or grow with text size. The outlined and native variants keep their existing layout.
Add multiple to select several options. Chosen values appear as a comma-separated list. Distinct options with the same value can be selected and deselected independently.
Native <optgroup> elements become group headings in the menu.
Put an image URL in data-icon on an option. The component
copies that option's classes to the image. Images float right by
default, and left floats one left. The
<select> needs no icons class.
Supported URLs are relative, HTTP(S), blob, and image data URLs (AVIF,
BMP, GIF, JPEG, PNG, SVG, WebP, or ICO). The component ignores invalid
URLs and other schemes, and the option stays selectable.
Add browser-default to skip the menu and keep the native select.
<div class="field">
<select id="form-select-1">
<option value="" disabled selected>Choose your option</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
<label for="form-select-1">Expressive Select</label>
</div>
<div class="field">
<select id="form-select-2" multiple>
<option value="" disabled selected>Choose your option</option>
<option value="1">Option 1</option>
<option value="2">Option 2</option>
<option value="3">Option 3</option>
</select>
<label for="form-select-2">Multiple Select</label>
</div>
<div class="field">
<select id="form-select-3">
<optgroup label="team 1">
<option value="1">Option 1</option>
<option value="2">Option 2</option>
</optgroup>
<optgroup label="team 2">
<option value="3">Option 3</option>
<option value="4">Option 4</option>
</optgroup>
</select>
<label for="form-select-3">Optgroups</label>
</div>
<div class="field">
<select id="form-select-4">
<option value="" disabled selected>Choose your option</option>
<option value="1" data-icon="photo.jpg">example 1</option>
<option value="2" data-icon="photo.jpg" class="left">example 2</option>
</select>
<label for="form-select-4">Images in select</label>
</div>
<label for="form-select-6">Browser Select</label>
<select id="form-select-6" class="browser-default">
<option value="" disabled selected>Choose your option</option>
<option value="1">Option 1</option>
</select>
Initialization
The IIFE bundle exposes Expressive.FormSelect.
Call init yourself when you need options other than the defaults,
after adding a select dynamically, or after changing an existing select's options.
Otherwise Expressive.AutoInit() starts every select.
document.addEventListener('DOMContentLoaded', function() {
const elems = document.querySelectorAll('select');
const instances = Expressive.FormSelect.init(elems, {
// specify options here
});
});
You can also pass per-instance options through AutoInit:
Expressive.AutoInit(document.body, {
FormSelect: { menuOptions: { constrainWidth: false } }
});
Search
Set searchable: true for a single or multiple select, or use the existing
data-form-select-options attribute. A separate search field in
the popup filters option text. The read-only trigger keeps showing the chosen
value, and the native select still supplies the form value.
<select name="destination" aria-label="Destination"
data-form-select-options='{"searchable":true}'>
<optgroup label="Europe">
<option value="paris">Paris</option>
<option value="rome">Rome</option>
</optgroup>
</select>
// Or initialize from script:
Expressive.FormSelect.init(select, { searchable: true });
Matching uses a case-insensitive literal substring of option text, with leading and trailing query whitespace trimmed. Values, group names and accents are not substituted. Empty groups disappear. Matching disabled choices stay visible and unavailable. Typing never changes selection; the localized "No results" status appears only when no option matches.
The searchable trigger controls a named nonmodal dialog containing the search field and a separate named listbox. Search and status are outside the listbox. Search text has no form name. Closing clears the query; a successful reset closes and restores the native defaults. Refresh retains an open query, search focus and caret, or a surviving enabled result's focus.
| Key or action | Result |
|---|---|
| Open by pointer, Enter, Space or vertical arrow | Focus search, unless menuOptions.autoFocus is false. |
| Type, paste, Home, End, horizontal arrows, composition | Edit search normally. Enter in search neither chooses nor submits. |
| ArrowDown / ArrowUp from search | Focus the first / last enabled visible result. Stay in search if none exists. |
| Arrows, Home / End on a result | Move among enabled visible results without wrapping. Up from the first returns to search. |
| Enter / Space on a result | Single select: choose it, close and return focus to the trigger. Multiple select: toggle it and keep the popup open, the query and focus on the row. |
| Tab / Shift+Tab | Tab from search enters the first enabled result; Tab from a result exits to the next control. Without enabled results, Tab exits from search. Shift+Tab from a result returns to search; from search it closes and focuses the trigger. |
| Escape / outside click | Close without changing selection. Escape focuses the trigger; an outside click keeps focus on its target. |
On a multiple select, the listbox is multi-selectable and each pick fires one
change. The popup stays open with the query unchanged, so several
matches can be picked in a row. An option the query hides stays selected,
stays in the trigger text and is submitted. Escape, Tab or an outside click
closes the popup, clears the query and keeps every selection.
Search is off by default; browser-default selects stay native.
TreeSelect supports single and multiple leaf search.
Remote search, creating options, fuzzy matching and virtualization
are outside this mode. With autoFocus: false, opening keeps
existing focus and Tab from the trigger can still reach search.
Options
| Name | Type | Default | Description |
|---|---|---|---|
searchable | Boolean | false | Add a popup search field to an enhanced plain single or multiple select. |
i18n | Object | searchLabel: "Search options", noResults: "No results" | Override either search label or empty-result message. Omitted strings retain their defaults. |
classes |
String | '' |
Space-separated classes added to the .field wrapper, either the parent .field or one the plugin creates. |
menuOptions |
Object | {} |
Options passed to Menu. See
Menu.
The component forces coverTrigger and
closeOnClick to false.
|
There is no selected option. Mark options with the HTML selected attribute instead.
Methods
All methods are called on the plugin instance. You can get the instance like this:
const instance = Expressive.FormSelect.getInstance(elem);
.getSelectedValues();
Returns the selected values as an array of strings.
instance.getSelectedValues();
.refresh();
Re-read the native <select>. Call this after changing
select.value, option labels, options or disabled states.
It rebuilds the menu rows and leaves the field and any existing Menu
instance in place. Refreshing an open menu keeps focus on a surviving option.
These programmatic updates do not emit a user change event.
instance.refresh();
.destroy();
Destroy the plugin instance, remove the menu, and restore the native select. Associated labels keep their original nodes and event listeners. Teardown restores their attributes and authored positions.
instance.destroy();
Native forms
The native select handles selection and form submission. The generated menu does not let users choose disabled options or options in disabled optgroups. A selected disabled option stays visible in the field, but native form submission excludes it. An empty selection displays an empty field.
The enhanced field exposes one combobox to assistive technology. All associated
labels contribute to its name in their original order, unless an authored
aria-label or aria-labelledby supplies the name. Each label
targets the visible control. While initialized, the native select stays
visually hidden and out of the accessibility tree.
Required and custom validation errors use the native select's
browser-provided message. The component shows it as supporting text
associated with the visible field. Native validation chooses which field
receives focus, and the enhanced select forwards that focus to its visible
field. checkValidity() preserves focus. reportValidity()
on a select reports that control even when an earlier field is invalid.
After changing setCustomValidity() from script, call
refresh() to update an existing error. Correcting the value clears
resolved errors. Resetting the form clears displayed errors without changing
native validation rules. destroy() removes generated error messages
and restores the native control.
Use the form's Reset button or form.reset() to restore defaults.
The generated field synchronizes after the browser resets the native control,
without a change event. Canceling the reset preserves selection.
Properties
| Name | Type | Description |
|---|---|---|
el |
Element | The native <select> the plugin was initialized with. |
options |
Object | The options the instance was initialized with. |
isMultiple |
Boolean | Whether this is a multiple select. |
wrapper |
Element | The .field wrapper, either the parent .field or one the plugin creates. |
menuEl |
Element | The generated popup. A <menu> by default, or a dialog <div> containing a listbox when searchable. |
labelEl |
Element | The associated label, or null if none was found. |
input |
Element | The text input that shows the current selection. |
menu |
Menu | The Menu instance for this select. |
Disabled styles
disabled on the <select> disables the whole control.
disabled on an <option> makes that item unselectable.
<div class="field">
<select id="form-select-7" disabled>
<option value="" disabled selected>Choose your option</option>
<option value="1">Option 1</option>
</select>
<label for="form-select-7">Disabled Select</label>
</div>
<label for="form-select-8">Browser Disabled</label>
<select id="form-select-8" class="browser-default" disabled>
<option value="" disabled selected>Choose your option</option>
<option value="1">Option 1</option>
</select>