<u-combobox>
<u-combobox>
extends <datalist>
with support for multiselect, separate label and programmatic value and clear button. While these features are not yet part of any official ARIA pattern or HTML element, <u-combobox>
adhere closely to HTML conventions.
BETA
u-combobox
is work in progress. Changes might apply and documentation is not complete.
Quick intro:
- Use
<data>
as direct child elements - these are the removable items - Use
<input>
and<u-datalist>
to allow adding and suggesting items - Use
<del>
betweeninput
anddatalist
to create a clear button - Use
data-multiple
oto allow selecting multiple items - Use
data-creatable
to allow creating items not in the list - Use
data-*
attributes to translate screen reader announcements - Use
beforechange
,afterchange
andbeforematch
events to manipulate state - Add
<select>
as child forFormData
or form submittion compatibility - Use matching
id
on<input>
andfor
attribute on<label>
to connect - MDN Web Docs: <data> (HTMLDataElement) / <input> (HTMLInputElement) / <datalist> (HTMLDatalistElement) / <option> (HTMLOptionElement)
Example
<label for="my-input"> Choose flavor of ice cream </label> <u-combobox data-multiple> <data>Coconut</data> <data>Banana</data> <data>Pineapple</data> <data>Orange</data> <input id="my-input" list="my-list" /> <del aria-label="Clear text">×</del> <u-datalist id="my-list" data-sr-singular="%d flavor" data-sr-plural="%d flavours"> <u-option>Coconut</u-option> <u-option>Strawberries</u-option> <u-option>Chocolate</u-option> <u-option>Vanilla</u-option> <u-option>Licorice</u-option> <u-option>Pistachios</u-option> <u-option>Mango</u-option> <u-option>Hazelnut</u-option> </u-datalist> </u-combobox> <style> /* Styling just for example: */ u-combobox { border: 1px solid; display: flex; flex-wrap: wrap; gap: .5em; padding: .5em; position: relative } u-option[selected] { font-weight: bold } u-datalist { position: absolute z-index: 9; inset: 100% -1px auto; border: 1px solid; background: white; padding: .5em } </style>
Install
bash
npm add -S @u-elements/u-combobox
bash
pnpm add -S @u-elements/u-combobox
bash
yarn add @u-elements/u-combobox
bash
bun add -S @u-elements/u-combobox
html
<script type="module" src="https://unpkg.com/@u-elements/u-combobox@latest/dist/u-combobox.js"></script>
Attributes and props
<u-combobox>
- Attributes: all global HTML attributes such as
id
,class
,data-
data-sr-added
prefixes announcements about additions. Defaults to"Added"
data-sr-removed
prefixes announcements about removals. Defaults to"Removed"
data-sr-remove
announces ability to remove. Defaults to"Press to remove"
data-sr-empty
announces no selected items. Defaults to"No selected"
data-sr-found
announces where to find amount of selected items. Defaults to"Navigate left to find %d selected"
data-sr-invalid
announces if trying to select invalid value. Defaults to"Invalid value""
data-sr-of
separates "number of total" in announcements. Defaults to"of"
- DOM interface:
UHTMLComboboxElement
extendsHTMLElement
UHTMLComboboxElement.control
returnsHTMLInputElement | null
UHTMLComboboxElement.items
returnsHTMLCollectionOf<HTMLDataElement>
UHTMLComboboxElement.list
returnsHTMLDataListElement | null
UHTMLComboboxElement.options
returnsHTMLCollectionOf<HTMLOptionElement> | undefined
UHTMLComboboxElement.values
returnsstring[]
withvalue
of each item
<data>
- Attributes: all global HTML attributes such as
id
,class
,data-
value
optionally specify the machine-readable translation of the text content.
- DOM interface:
HTMLDataElement
HTMLDataElement.value
string reflecting thevalue
HTML attribute.
Events
In addition to the usual events supported by HTML elements, the <u-combobox>
elements dispatches custom events allowing you to affect state:
beforechange
js
myCombobox.addEventListener('beforechange', (event) => {
event.target // UHTMLComboboxElement
event.detail // HTMLDataElement to add or remove
event.detail.isConnnected // true if removing, false if adding
event.preventDefault() // Optionally prevent action
})
afterchange
js
myCombobox.addEventListener('afterchange', (event) => {
event.target // UHTMLComboboxElement
event.detail // HTMLDataElement added or removed
event.detail.isConnnected // false if removing, true if adding
})
beforematch
js
myCombobox.addEventListener('beforematch', (event) => {
event.target // UHTMLComboboxElement
event.detail // HTMLOptionElement | undefined match in option list
// You can change match by looping options and setting option.selected:
// for (const opt of event.target.options) opt.selected = your-condition-here;
})
Styling
<u-combobox>
renders as display: block
, while <data>
renders as display: inline-block
with a ::after
element to render the removal ×
.
Example: Norwegian
<label for="my-norwegian-input"> Velg type iskrem </label> <u-combobox data-sr-added="La til" data-sr-remove="Trykk for å fjerne" data-sr-removed="Fjernet" data-sr-empty="Ingen valgte" data-sr-found="Naviger til venstre for å finne %d valgte" data-sr-of="av" > <data>Kokkos</data> <input id="my-norwegian-input" list="my-norwegian-list" /> <del aria-label="Fjern tekst">×</del> <u-datalist id="my-norwegian-list" data-sr-singular="%d smak" data-sr-plural="%d smaker"> <u-option>Kokkos</u-option> <u-option>Jordbær</u-option> <u-option>Sjokolade</u-option> <u-option>Vanilje</u-option> <u-option>Lakris</u-option> <u-option>Pistasj</u-option> <u-option>Mango</u-option> <u-option>Hasselnøtt</u-option> </u-datalist> </u-combobox> <style> /* Styling just for example: */ u-combobox { border: 1px solid; display: flex; flex-wrap: wrap; gap: .5em; padding: .5em; position: relative } u-option[selected] { font-weight: bold } u-datalist { position: absolute; z-index: 9; inset: 100% -1px auto; border: 1px solid; background: white; padding: .5em } </style>
Example: Custom matching
<label for="my-matching-input"> Matches from start of word </label> <br> <small>Try typing "c" and hitting "Enter"</small> <u-combobox id="my-matching-combobox"> <input id="my-matching-input" list="my-matching-list" /> <del aria-label="Clear text">×</del> <u-datalist id="my-matching-list"> <u-option>Coconut</u-option> <u-option>Strawberries</u-option> <u-option>Chocolate</u-option> <u-option>Vanilla</u-option> </u-datalist> </u-combobox> <script type="module"> const combobox = document.getElementById('my-matching-combobox'); combobox.addEventListener('beforematch', (event) => { event.preventDefault(); const input = combobox.control; const query = input.value.toLowerCase().trim(); for(const opt of input.list.options) { opt.selected = !!query && opt.label.toLowerCase().trim().startsWith(query); } }); </script> <style> /* Styling just for example: */ u-combobox { border: 1px solid; display: flex; flex-wrap: wrap; gap: .5em; padding: .5em; position: relative } u-option[selected] { font-weight: bold } u-datalist { position: absolute; z-index: 9; inset: 100% -1px auto; border: 1px solid; background: white; padding: .5em } </style>
Example: Custom filtering
Notice: <u-datalist>
has data-nofilter
to allow custom filtering
<label for="my-filtering-input"> Filters case sensitive </label> <br> <small>Try typing "v" versus "V"</small> <u-combobox id="my-filtering-combobox"> <input id="my-matching-input" list="my-filtering-list" /> <u-datalist data-nofilter id="my-filtering-list"> <u-option>Coconut</u-option> <u-option>Strawberries</u-option> <u-option>Chocolate</u-option> <u-option>Vanilla</u-option> </u-datalist> </u-combobox> <script type="module"> const combobox = document.getElementById('my-filtering-combobox'); combobox.addEventListener('input', (event) => { event.preventDefault(); const input = combobox.control; const query = input.value.trim(); for(const opt of input.list.options) { opt.hidden = !!query && !opt.label.trim().includes(query); } }); </script> <style> /* Styling just for example: */ u-combobox { border: 1px solid; display: flex; flex-wrap: wrap; gap: .5em; padding: .5em; position: relative } u-option[selected] { font-weight: bold } u-datalist { position: absolute; z-index: 9; inset: 100% -1px auto; border: 1px solid; background: white; padding: .5em } </style>
Example: API results
<label for="my-api-input"> Search for a country </label> <u-combobox id="my-api-combobox"> <input id="my-api-input" list="my-api-list" /> <del aria-label="Clear text">×</del> <u-datalist id="my-api-list" data-nofilter> <u-option value="">Type to search...</u-option> </u-datalist> </u-combobox> <script type="module"> const combobox = document.getElementById('my-api-combobox'); const xhr = new XMLHttpRequest(); // Easy to abort let debounceTimer; // Debounce so we do not spam API // Same handler every time xhr.addEventListener('load', () => { const list = combobox.control.list; try { list.replaceChildren(...JSON.parse(xhr.responseText).map((country) => { const option = document.createElement('u-option'); option.text = country.name; return option; })); } catch (err) { list.innerHTML = '<u-option value="">No results</u-option>'; } }); combobox.addEventListener('input', (event) => { const { list, value } = combobox.control; const query = encodeURIComponent(value.trim()); list.innerHTML = query ? '<u-option value="">Loading</u-option>' : ''; xhr.abort(); clearTimeout(debounceTimer); if (query) { debounceTimer = setTimeout(() => { xhr.open('GET', `https://restcountries.com/v2/name/${query}?fields=name`, true); xhr.send(); }, 600); } }); </script> <style> /* Styling just for example: */ u-combobox { border: 1px solid; display: flex; flex-wrap: wrap; gap: .5em; padding: .5em; position: relative } u-option[selected] { font-weight: bold } u-datalist { position: absolute; z-index: 9; inset: 100% -1px auto; border: 1px solid; background: white; padding: .5em } </style>
Accessibility
Screen reader | <u-combobox> |
---|---|
VoiceOver (Mac) + Chrome | ✅ |
VoiceOver (Mac) + Edge | ✅ |
VoiceOver (Mac) + Firefox | ✅ |
VoiceOver (Mac) + Safari | ✅ |
VoiceOver (iOS) + Safari | ✅ |
Jaws (PC) + Chrome | ✅ |
Jaws (PC) + Edge | ✅ |
Jaws (PC) + Firefox | ✅ |
NVDA (PC) + Chrome | ✅ |
NVDA (PC) + Edge | ✅ |
NVDA (PC) + Firefox | ✅ needs focus mode to announce item removal |
Narrator (PC) + Chrome | ✅ |
Narrator (PC) + Edge | ✅ |
Narrator (PC) + Firefox | ✅ |
TalkBack (Android) + Chrome | ✅ |
TalkBack (Android) + Firefox | ✅ |
TalkBack (Android) + Samsung Internet | ✅ |
Specifications
- DOM interface: HTMLElement
- HTML Standard: The <div> element
- DOM interface: HTMLDataElement
- HTML Standard: The <data> element
Changelog
- 0.0.18: Input value is now reverted instead of cleared when no match on blur/enter
- 0.0.17: Input now gets new
list
attributeid
ofdatalist
changes - 0.0.15: Sync input value when data-elements change and only trigger
beforechange
andafterchange
on click, enter or blur, but not while typing in single mode - 0.0.14: Fix issue where removing single element programmatically caused focus
- 0.0.13: Update sync state when options are changed
- 0.0.12: Always sync
<del>
with input value on mount - 0.0.11: Improved performance
- 0.0.10: Only remove
<data>
during change event for React compatiliby - 0.0.9: Improve browser compatibility by avoiding
toggleAttribute
- 0.0.8: Avoid hiding
<del>
when clicking option withoutvalue
- 0.0.7: Add support for
<del>
element to clear the input - 0.0.6: Ensure correct value of hidden
<select>
- 0.0.5: Prevent
input
value change whenbeforechange
is prevented - 0.0.4: Bugfix
- 0.0.3: Prevent add if
u-option
has empty value attribute - 0.0.2: Reset value when clicking option in multiple mode
- 0.0.1: Support async
u-option
initialization - 0.0.0: Beta release