Forms Select

Select

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.

HTML
<select class="wf-select" multiple data-wf-select
        data-wf-placeholder="Choose skills…"
        data-wf-clearable
        data-wf-max-visible="3"
        name="skills">
  <option value="html" selected>HTML</option>
  <option value="css"  selected>CSS</option>
  <option value="js"   selected>JavaScript</option>
  <option value="php">PHP</option>
</select>

Searchable

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 .

HTML
<select class="wf-select" data-wf-select
        data-wf-searchable
        data-wf-clearable
        data-wf-placeholder="Search a country…"
        name="country">
  <option value="de">Germany</option>
  <option value="fr">France</option>
  <option value="gb">United Kingdom</option>
  <option value="us">United States</option>
</select>

Option Groups

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.

HTML
<select class="wf-select" multiple data-wf-select data-wf-searchable>
  <optgroup label="Frontend">
    <option value="js"  selected>JavaScript</option>
    <option value="css">CSS</option>
  </optgroup>
  <optgroup label="Backend">
    <option value="php"    selected>PHP</option>
    <option value="python">Python</option>
  </optgroup>
</select>

Sizes & Pill

Set data-wf-size to sm or lg for size presets; the default is medium. Add data-wf-pill for a fully-rounded combobox.

HTML
<select class="wf-select" data-wf-select data-wf-size="sm">…</select>
<select class="wf-select" data-wf-select>…</select>
<select class="wf-select" data-wf-select data-wf-size="lg">…</select>
<select class="wf-select" data-wf-select data-wf-pill>…</select>

Placement

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>`
}