<u-datalist>
<u-datalist>
lets you suggest values to a connected <input>
. You can use it to make things like comboboxes, autosuggest, autocomplete, live search results, etc.
Quick intro:
- Use
<u-option>
as direct child elements - these will show the suggestions while typing - Use matching
id
on<u-datalist>
andlist
attribute on<input>
to connect - Want to show suggestions from a data source? See example: API →
- MDN Web Docs: <datalist> (HTMLDatalistElement) / <option> (HTMLOptionElement)
Example
<label for="my-input"> Choose flavor of ice cream </label> <input id="my-input" list="my-list" /> <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> <style> /* Styling just for example: */ u-option[selected] { font-weight: bold } </style>
Install
npm add -S @u-elements/u-datalist
pnpm add -S @u-elements/u-datalist
yarn add @u-elements/u-datalist
bun add -S @u-elements/u-datalist
<script type="module" src="https://unpkg.com/@u-elements/u-datalist@latest/dist/u-datalist.js"></script>
Attributes and props
<u-datalist>
- Attributes: all global HTML attributes such as
id
,class
,data-
id
must be identical to value oflist
attribute on associated<input>
- Screen reader attributes required to meet WCAG §4.1.3:
data-sr-singular="%d hit"
announces single hitdata-sr-plural="%d hits"
announces multiple hits- Note: If
<u-datalist>
has no options, visible text will be announced instead (i.e. No results)
- DOM interface:
HTMLDataListElement
HTMLDataListElement.options
returnsHTMLCollectionOf<HTMLOptionElement>
. However, note that there is inconsistency between Firefox and other browsers, regarding whetherdisabled
options are included or not. To ensure access to all options, consider using.children
instead.
<u-option>
- Attributes: all global HTML attributes such as
id
,class
,data-
disabled
disables and hides the element if present. Note: Settingdisabled="false"
will not work as intended, asdisabled
is a boolean attribute you should provide or remove entirely.label
label sets text indicating the meaning of the option. Iflabel
isn't defined, its value is that of the element text content.selected
sets option selected state. Note: Settingselected="false"
will not work as intended, asselected
is a boolean attribute you should provide or remove entirely.value
represents the value to be filled into associated<input>
. Ifvalue
isn't defined, the value is taken from the text content of the option element.
- DOM interface:
HTMLOptionElement
HTMLOptionElement.defaultSelected
returnstrue
orfalse
indicating stateHTMLOptionElement.disabled
returnstrue
offalse
reflecting thedisabled
attributeHTMLOptionElement.form
returns parentHTMLFormElement
HTMLOptionElement.index
returns anumber
representing the position/index within the list of optionsHTMLOptionElement.label
sets or getsstring
of optionlabel
HTMLOptionElement.selected
sets or getstrue
orfalse
indicating stateHTMLOptionElement.text
sets og getsstring
of option text contentHTMLOptionElement.value
sets og getsstring
of optionvalue
Events
While <u-datalist>
support all events, native datalist does not as it is rendered as part of the browser UI. Therefore, it's recommended to avoid binding events to <u-datalist>
or <u-option>
if you want to ensure native compatibility and future seamless opt-out.
Instead, you can detect option clicks by binding an input
listener to <input>
,
and use the utility import { isDatalistClick } from '@u-elements/u-datalist'
:
<input type="text" list="my-list" />
<u-datalist id="my-list">
<u-option>Option 1</u-option>
<u-option>Option 2</u-option>
</u-datalist>
<script type="module">
import { isDatalistClick } from '@u-elements/u-datalist';
const input = document.querySelector('input')
input.addEventListener('input', (event) => {
if (isDatalistClick(event)) {
// Event is triggered by user selecting an option in datalist
} else {
// Event is triggered by user typing in input
}
})
</script>
import { isDatalistClick } from '@u-elements/u-datalist';
const MyComponent = () => {
const handleChange = (event) => {
if (isDatalistClick(event.nativeEvent)) {
// Event is triggered by user selecting an option in datalist
} else {
// Event is triggered by user typing in input
}
};
return (
<input type="text" list="my-list" onChange={handleChange} />
<u-datalist id="my-list">
<u-option>Option 1</u-option>
<u-option>Option 2</u-option>
</u-datalist>
);
}
Styling
While <u-datalist>
and <u-option>
are styleable, native datalist and option elements are currently not. However, there is a possibility that the native elements may support styling in the future.
Styling with the display property
<u-datalist>
and <u-option>
are both rendered as display: block
when visible, and are hidden by the hidden
attribute. Styling with a display
property will override the hidden
attribute, thus disabling datalist show/hide and option filtering, unless you wrap your styling in a :not([hidden])
selector:
u-datalist:not([hidden]) {
display: flex;
/* Use :not([hidden]) if you wish to change u-datalist display
* without disabling open/close */
}
u-option:not([hidden]) {
display: flex;
/* Use :not([hidden]) if you wish to change u-option display
* without disabling filtering */
}
Styling option focus and selected state
<u-option>
receive real focus on keyboard navigation, and a selected
attribute on selection, which both can be utilized for styling:
u-option:focus {
/* Focused option styling here */
}
u-option:not(:focus) {
/* Un-focused option styling here */
}
u-option[selected] {
/* Selected option styling here */
}
u-option:not([selected]) {
/* Un-selected option styling here */
}
Styling example: Datalist position and animation
<style> .my-input, .my-list { background: #fff; border-radius: .25em; border: 2px solid #090C33; box-sizing: border-box; color: #090C33; font: inherit; padding: .5em; width: 13em; transition: .2s; /* Animate */ } .my-input { background: #fff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='none' stroke='%23000' stroke-width='2' d='m3 8 9 9 9-9'/%3E%3C/svg%3E") center right/2rem 1rem no-repeat; } .my-input[aria-expanded="true"] { background-color: #f9ffd7; border-bottom-left-radius: 0; border-bottom-right-radius: 0; } .my-input:focus-visible, .my-list u-option:focus { box-shadow: 0 0 0 1px #fff,0 0 0 3px #6325e7,0 0 0 4px #fff; outline: none; } .my-list { border-top-left-radius: 0; border-top-right-radius: 0; box-shadow: 0 .3em 1em #090C3333; display: block; /* Overwrites hidden attribute */ margin-top: -2px; position: absolute; } .my-list[hidden] { opacity: 0; translate: 0 -.5em; visibility: hidden; } .my-list u-option { border-radius: .1em; padding: .5em; transition: .2s; } .my-list u-option:focus { background-color: #EBF0FA; } .my-list u-option[selected] { font-weight: bold; text-decoration: underline; } </style> <label for="my-styling-input"> Choose flavor of ice cream <input type="text" id="my-styling-input" class="my-input" list="my-styling" /> </label> <u-datalist class="my-list" id="my-styling" 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-datalist> <p> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Phasellus tristique tellus massa, eget sollicitudin arcu luctus vel. Cras non purus accumsan, ultricies mi ut, blandit magna. </p>
Example: Dynamic
<label for="my-dynamic-input">Choose your email</label> <input type="text" id="my-dynamic-input" list="my-dynamic-list" placeholder="Type email..." /> <u-datalist id="my-dynamic-list" data-sr-singular="%d suggestion" data-sr-plural="%d suggestions"> </u-datalist> <script type="module"> import { isDatalistClick } from '@u-elements/u-datalist'; const input = document.getElementById('my-dynamic-input'); input.addEventListener('input', (event) => { if (isDatalistClick(event)) return; // User clicked option element const value = input.value.split("@")[0]; const values = [ `${value}@live.com`, `${value}@icloud.com`, `${value}@hotmail.com`, `${value}@gmail.com`, ]; if (!value) input.list?.replaceChildren(''); else input.list?.replaceChildren(...values.map((text) => { return Object.assign(document.createElement("u-option"), { text }); })); }); </script>
Example: Link
<label for="my-link-input"> Open u-element documentation </label> <input type="text" id="my-link-input" list="my-link-list" /> <u-datalist id="my-link-list" data-sr-singular="%d result" data-sr-plural="%d results"> <u-option value="https://u-elements.github.io/u-elements/elements/u-datalist">u-datalist</u-option> <u-option value="https://u-elements.github.io/u-elements/elements/u-details">u-details</u-option> <u-option value="https://u-elements.github.io/u-elements/elements/u-dialog">u-dialog</u-option> <u-option value="https://u-elements.github.io/u-elements/elements/u-progress">u-progress</u-option> <u-option value="https://u-elements.github.io/u-elements/elements/u-select">u-select</u-option> <u-option value="https://u-elements.github.io/u-elements/elements/u-tabs">u-tabs</u-option> <u-option value="https://u-elements.github.io/u-elements/elements/u-tags">u-tags</u-option> </u-datalist> <script type="module"> import { isDatalistClick } from '@u-elements/u-datalist'; const input = document.getElementById('my-link-input'); input.addEventListener('input', (event) => { if (isDatalistClick(event)) { // User clicked option element window.location.href = input.value; input.value = ''; // Clear input } }); </script>
Example: Custom filter
- Native
<datalist>
does not support custom filtering/content. Therefore, u-elements provide a utilityimport { syncDatalistState } from '@u-elements/u-datalist'
. - Call
syncDatalistState(input)
after you manually disable/enable options of your choise, to show all non-disabled options syncDatalistState
is both compatible with<u-datalist>
and native<datalist>
- Note:
value
might be re-written to acheive custom rendering. UsegetDatalistValue(input)
orgetDatalistValue(option)
to access the original value - Note: If you're using React, you can call
syncDatalistState
in auseEffect(() => syncDatalistState(inputRef.current))
;
<label for="my-filter-input"> Custom filter (search from start only) </label> <input type="text" id="my-filter-input" list="my-filter-list" /> <u-datalist id="my-filter-list" data-sr-singular="%d result" data-sr-plural="%d results"> <u-option>u-datalist</u-option> <u-option>u-details</u-option> <u-option>u-dialog</u-option> <u-option>u-progress</u-option> <u-option>u-select</u-option> <u-option>u-tabs</u-option> <u-option>u-tags</u-option> </u-datalist> <script type="module"> import { isDatalistClick, syncDatalistState } from '@u-elements/u-datalist'; const input = document.getElementById('my-filter-input'); // Achieve custom filter on native datalist input.addEventListener("input", (event) => { if (isDatalistClick(event)) return; // User clicked option element // Your custom filtering here: const needle = event.target.value.trim().toLowerCase(); const options = event.target.list.children; for (const option of options) { option.disabled = !option.text.toLowerCase().startsWith(needle); } // Must run last syncDatalistState(input); }); </script>
Example: API
- Native
<datalist>
does not support custom filtering/content. Therefore, u-elements provide a utilityimport { syncDatalistState } from '@u-elements/u-datalist'
. - Call
syncDatalistState(input)
after you set HTML, to show all non-disabled options syncDatalistState
is both compatible with<u-datalist>
and native<datalist>
- Note:
value
might be re-written to acheive custom rendering. UsegetDatalistValue(input)
orgetDatalistValue(option)
to access the original value - Note: If you're using React, you can call
syncDatalistState
in auseEffect(() => syncDatalistState(inputRef.current))
;
<label for="my-api-input"> Search for country </label> <input type="text" id="my-api-input" list="my-api-list" /> <u-datalist id="my-api-list" data-sr-singular="%d country" data-sr-plural="%d countries"></u-datalist> <script type="module"> import { isDatalistClick, syncDatalistState } from '@u-elements/u-datalist'; let debounceTimer; // Debounce so we do not spam API const input = document.getElementById('my-api-input'); const list = input.list; const xhr = new XMLHttpRequest(); // Easy to abort // Same handler every time xhr.onload = () => { try { list.replaceChildren(...JSON.parse(xhr.responseText).map((country) => { const option = document.createElement('u-option'); option.text = country.name; return option; })); } catch (err) { // Using role="none" to avoid getting counted as a hit list.innerHTML = 'No results '; } syncDatalistState(input); }; input.addEventListener('input', (event) => { if (isDatalistClick(event)) return; // User clicked option element // Using role="none" to avoid getting counted as a hit const value = encodeURIComponent(input.value.trim()); list.innerHTML = 'Loading '; xhr.abort(); clearTimeout(debounceTimer); debounceTimer = setTimeout(() => { if (!value) return; xhr.open('GET', `https://restcountries.com/v2/name/${value}?fields=name`, true); xhr.send(); }, 300); syncDatalistState(input); }); </script>
Accessibility
Screen reader | <datalist> | <u-datalist> |
---|---|---|
VoiceOver (Mac) + Chrome | ❌ Does not announce option count | ✅ |
VoiceOver (Mac) + Edge | ✅ | ✅ |
VoiceOver (Mac) + Firefox | ❌ Does not announce option count | ✅ |
VoiceOver (Mac) + Safari | ❌ Does not announce option count | ✅ |
VoiceOver (iOS) + Chrome | ❌ Does not announce options | ✅ |
VoiceOver (iOS) + Safari | ❌ Does not announce options | ✅ |
Jaws (PC) + Chrome | ✅ | ✅ |
Jaws (PC) + Edge | ✅ | ✅ |
Jaws (PC) + Firefox | ✅ | ✅ |
NVDA (PC) + Chrome | ✅ | ✅ |
NVDA (PC) + Edge | ✅ | ✅ |
NVDA (PC) + Firefox | ✅ | ✅ |
Narrator (PC) + Chrome | ✅ | ✅ |
Narrator (PC) + Edge | ✅ | ✅ |
Narrator (PC) + Firefox | ❌ Does not show options | ✅ |
TalkBack (Android) + Chrome | ✅ | ✅ |
TalkBack (Android) + Firefox | ❌ Does not show options | ✅ |
TalkBack (Android) + Samsung Internet | ✅ | ✅ |
Specifications
- DOM interface: HTMLDatalistElement
- HTML Standard: The <datalist> element
- DOM interface: HTMLOptionElement
- HTML Standard: The <option> element