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