<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.
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
comboboxbeforeselect
,comboboxafterselect
andcomboboxbeforematch
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
npm add -S @u-elements/u-combobox
pnpm add -S @u-elements/u-combobox
yarn add @u-elements/u-combobox
bun add -S @u-elements/u-combobox
<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:
comboboxbeforeselect
myCombobox.addEventListener('comboboxbeforeselect', (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
})
comboboxafterselect
myCombobox.addEventListener('comboboxafterselect', (event) => {
event.target // UHTMLComboboxElement
event.detail // HTMLDataElement added or removed
event.detail.isConnnected // false if removing, true if adding
})
comboboxbeforematch
myCombobox.addEventListener('comboboxbeforematch', (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('comboboxbeforematch', (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>
Example: Only filter during typing
<label for="my-typing-input"> All results will be visible on load and after selection: </label> <u-combobox id="my-typing-combobox"> <input id="my-typing-input" list="my-typing-list" /> <del aria-label="Clear text">×</del> <u-datalist id="my-typing-list" data-nofilter> <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-typing-combobox'); combobox.addEventListener('input', (event) => { // Only user can cause trusted typing events combobox.list.toggleAttribute('data-nofilter', !event.isTrusted); }); </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: Controlled render
u-combobox
adds and removes <data>
elements, which can be confusing for frameworks such as React.
If you encounter issues, it’s recommended to call event.preventDefault()
inside the comboboxbeforeselect
handler,
and then manually add or remove <data>
elements as needed:
<label for="my-controlled-input"> Controlled render: </label> <u-combobox id="my-controlled-combobox" data-multiple> <!-- Your framework rendering here, i.e. React: {values.map(({ value, label }) => ( <data key={value} value={value}> {label} </data> ))} --> <input id="my-controlled-input" list="my-controlled-list" /> <del aria-label="Clear text">×</del> <u-datalist id="my-controlled-list" data-nofilter> <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-controlled-combobox'); const [values, setValues] = useState([]); combobox.addEventListener('comboboxbeforeselect', (event) => { event.preventDefault(); // Stops u-combobox const data = event.detail; const label = data.textContent; const value = data.value; const isAdd = !data.isConnected; // Add/remove: if (isAdd) setValues([...values, { label, value }]); else setValues(values.filter((item) => item.value !== value)); }); </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
Server side rendering
You can server side render <u-combobox>
by using Declarative Shadow DOM.
Styling and markup needed is exported as UHTMLComboboxShadowRoot
. Example:
import { UHTMLComboboxShadowRoot } from '@u-elements/u-combobox';
import { UHTMLDataListShadowRoot } from '@u-elements/u-datalist';
const renderToStaticMarkup = (data: string, options: string) =>
`<label for="my-ssr-input">Server side rendered</label>
<u-combobox>
${UHTMLComboboxShadowRoot}
${data}
<input id="my-ssr-input" />
<u-datalist>
${UHTMLDataListShadowRoot}
${options}
</u-datalist>
</u-combobox>`
Changelog
- 1.0.1: Support setting
aria-label
on input and enable declarative shadow root support and exportUHTMLComboboxShadowRoot
for easier server side rendering - 1.0.0: Renamed events to avoid conflict with native events:
afterchange
=>comboboxafterselect
beforechange
=>comboboxbeforeselect
beforematch
=>comboboxbeforematch
- 0.0.20: Clean up unused comma from
aria-label
when single mode - 0.0.19: Respects input
disabled
andreadonly
and moves caret to end of text onarrow up
- 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