Progressive enhancement of a native
<select>
element via
wf-select.js
. The native element stays in the DOM hidden, so forms and server submissions require zero changes. Supports single and multiple modes, tag chips, option groups, type-ahead search, a clearable button, auto or forced panel direction, size variants, and pill style.
Quick Start
Import the module once. All
<select data-wf-select>
elements are enhanced automatically on
DOMContentLoaded
. The native element is hidden, and a custom combobox is inserted after it. Add
data-wf-clearable
to show a × button when a value is selected.
JS
import './wf-select.js'
HTML
<!-- Auto-init -->
<select class="wf-select" data-wf-select name="color"
data-wf-placeholder="Pick a color…"
data-wf-clearable>
<option value="red">Red</option>
<option value="blue" selected>Blue</option>
<option value="green">Green</option>
</select>
<!-- Manual init -->
<select id="my-select" class="wf-select">…</select>
<script type="module">
import { WojoSelect } from './wf-select.js'
const sel = new WojoSelect(document.getElementById('my-select')).init()
</script>
Multiple
Add the native
multiple
attribute to enable multi-select. Selected options are shown as removable tag chips. Use
data-wf-max-visible
to cap how many chips are shown before a
+N
overflow badge appears. Set to
"0"
to show all chips.
Add
data-wf-searchable
to show a filter input at the top of the listbox. Typing filters visible options by their text label. Works in both single and multiple modes. Printable-character type-ahead navigation is always active even without
data-wf-searchable
.
Wrap options in native
<optgroup>
elements. The component renders each group’s
label
as a non-interactive heading above its options. Works in both single and multiple modes, with or without search.
Control which side the listbox opens on with
data-wf-placement
. The default
auto
measures available viewport space and picks whichever side has more room, preferring below. Use
top
or
bottom
to force a direction. Adjust the gap with
data-wf-distance
(px) or the CSS variable
--wf-select-distance
.
Force upward
Force downward
HTML
<!-- Auto (default): picks the side with more room -->
<select class="wf-select" data-wf-select data-wf-placement="auto">…</select>
<!-- Force upward -->
<select class="wf-select" data-wf-select data-wf-placement="top">…</select>
<!-- Force downward -->
<select class="wf-select" data-wf-select data-wf-placement="bottom">…</select>
<!-- Custom gap in px -->
<select class="wf-select" data-wf-select data-wf-distance="8">…</select>
i18n
Register a locale globally with
WojoI18n
before
DOMContentLoaded
. Any key you omit falls back to the built-in English default. Strings support the tokens
{n}
(overflow badge count) and
{label}
(option text in tag remove aria-label).
For one-off text overrides, set the
data-wf-i18n-*
attributes directly on the
<select>
. These take the highest priority.
JS
import { WojoI18n } from './wf-base.js'
WojoI18n.setLocale('fr')
WojoI18n.register('fr', {
select: {
placeholder: 'Sélectionner une option',
searchLabel: 'Rechercher des options',
searchPlaceholder: 'Rechercher…',
clearLabel: 'Effacer la sélection',
removeTagLabel: 'Retirer {label}',
moreSelectedLabel: '{n} de plus',
noResultsSearch: 'Aucun résultat.',
noOptionsAvailable: 'Aucune option disponible.',
}
})
HTML
<!-- Per-instance overrides (highest priority) -->
<select class="wf-select" data-wf-select
data-wf-i18n-placeholder="Wählen Sie eine Option"
data-wf-i18n-search-placeholder="Suchen…"
data-wf-i18n-no-results-search="Keine Ergebnisse."
data-wf-i18n-no-options-available="Keine Optionen.">
…
</select>
Data Attributes
Place all attributes on the native
<select>
element.
Attribute
Values
Description
data-wf-select
—
Required. Marks the element for enhancement; auto-initializes on DOMContentLoaded.
data-wf-placeholder
string
Text shown in the combobox when nothing is selected.
data-wf-clearable
—
Show a × clear button when a value is selected.
data-wf-searchable
—
Show a text filter input when the listbox opens.
data-wf-max-visible
integer
Multiple mode: max tag chips shown before a +N overflow badge. 0 = show all. Default: 3.
data-wf-placement
auto · top · bottom
Preferred listbox direction. auto picks the side with more viewport space. Default: auto.
data-wf-distance
integer (px)
Gap between the combobox field and the listbox panel. Default: 4.
data-wf-pill
—
Fully rounded pill style for the combobox.
data-wf-size
sm · lg
Size preset. Omit for medium (default).
data-wf-exclusive
true · false
Close other WojoSelect instances when this one opens. Default: true.
Show More
CSS Custom Properties
Override on the wrapper element or any ancestor.
Property
Default
Description
--wf-select-tag-max-width
12ch
Maximum tag text width before truncation with ellipsis.
--wf-select-z
800
z-index of the listbox panel.
--wf-select-distance
4px
Gap between the combobox and listbox. Overridden by data-wf-distance.
Events & API
All events are dispatched on the native
<select>
element and bubble up the DOM. The instance is stored on
el._wfSelect
.
Events
Event
Cancelable
detail
Description
wf:show
Yes
—
Fired when open() is called. Cancel to prevent opening.
wf:after-show
No
—
Fired after the listbox becomes visible.
wf:hide
Yes
—
Fired when close() is called. Cancel to prevent closing.
wf:after-hide
No
—
Fired after the listbox is hidden.
wf:change
No
{ value }
Fired on selection change. value is a string (single) or string[] (multiple).
wf:clear
No
—
Fired when clear() empties the selection.
Methods
Method
Returns
Description
sel.open()
—
Open the listbox panel.
sel.close()
—
Close the listbox panel.
sel.toggle()
—
Toggle the listbox open or closed.
sel.clear()
—
Deselect all options and fire wf:clear.
sel.getValue()
string | string[]
Return the selected value (single) or array of values (multiple).
sel.setValue(v)
—
Set the selection programmatically; fires wf:change. Accepts a string or string[].
sel.update()
—
Re-read options from the native <select>. Call after dynamically adding or removing <option> elements.
sel.destroy()
—
Remove the custom UI and restore the native <select>.
JS
const el = document.querySelector('#my-select')
el.addEventListener('wf:change', (e) => console.log('changed', e.detail.value))
el.addEventListener('wf:clear', () => console.log('cleared'))
// Prevent opening conditionally
el.addEventListener('wf:show', (e) => {
if (someCondition) e.preventDefault()
})
// Programmatic control
const sel = el._wfSelect
sel.setValue('blue') // single
sel.setValue(['js', 'css']) // multiple
console.log(sel.getValue())
// Refresh after dynamic option changes
const opt = document.createElement('option')
opt.value = 'rust'; opt.text = 'Rust'
el.appendChild(opt)
sel.update()
// Custom tag renderer (multiple mode)
// Sanitise any user-supplied content before returning HTML
sel.getTag = (option, index) => {
return `<span class="my-tag" data-value="${option.value}">${option.text}</span>`
}