<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>betweeninputanddatalistto create a clear button - Use
data-multipleoto allow selecting multiple items - Use
data-creatableto allow creating items not in the list - Use
data-*attributes to translate screen reader announcements - Use
comboboxbeforeselect,comboboxafterselectandcomboboxbeforematchevents to manipulate state - Add
<select>as child forFormDataor form submittion compatibility - Use matching
idon<input>andforattribute 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-comboboxpnpm add -S @u-elements/u-comboboxyarn add @u-elements/u-comboboxbun 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-addedprefixes announcements about additions. Defaults to"Added"data-sr-removedprefixes announcements about removals. Defaults to"Removed"data-sr-removeannounces ability to remove. Defaults to"Press to remove"data-sr-emptyannounces no selected items. Defaults to"No selected"data-sr-foundannounces where to find amount of selected items. Defaults to"Navigate left to find %d selected"data-sr-invalidannounces if trying to select invalid value. Defaults to"Invalid value""data-sr-ofseparates "number of total" in announcements. Defaults to"of"
- DOM interface:
UHTMLComboboxElementextendsHTMLElementUHTMLComboboxElement.controlreturnsHTMLInputElement | nullUHTMLComboboxElement.itemsreturnsHTMLCollectionOf<HTMLDataElement>UHTMLComboboxElement.listreturnsHTMLDataListElement | nullUHTMLComboboxElement.optionsreturnsHTMLCollectionOf<HTMLOptionElement> | undefinedUHTMLComboboxElement.valuesreturnsstring[]withvalueof each item
<data>
- Attributes: all global HTML attributes such as
id,class,data-valueoptionally specify the machine-readable translation of the text content.
- DOM interface:
HTMLDataElementHTMLDataElement.valuestring reflecting thevalueHTML 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.4: Prevent focus trap on Safari caused by
tabindex="-1"on<del> - 1.0.3: Let
Deletekey do native text deletion instead of chip removal - 1.0.2: Fix sync issue when single mode and deleting
<data>element - 1.0.1: Support setting
aria-labelon input and enable declarative shadow root support and exportUHTMLComboboxShadowRootfor easier server side rendering - 1.0.0: Renamed events to avoid conflict with native events:
afterchange=>comboboxafterselectbeforechange=>comboboxbeforeselectbeforematch=>comboboxbeforematch- 0.0.20: Clean up unused comma from
aria-labelwhen single mode - 0.0.19: Respects input
disabledandreadonlyand 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
listattributeidofdatalistchanges - 0.0.15: Sync input value when data-elements change and only trigger
beforechangeandafterchangeon 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
inputvalue change whenbeforechangeis prevented - 0.0.4: Bugfix
- 0.0.3: Prevent add if
u-optionhas empty value attribute - 0.0.2: Reset value when clicking option in multiple mode
- 0.0.1: Support async
u-optioninitialization - 0.0.0: Beta release