Autocomplete
A text input with suggestions. The value is free text: any typed text is kept, a suggestion only saves typing.
Suggestions from a datalist
Wrap a text input in
<div class="autocomplete"> and give it a
<datalist>, linked with the input's
list attribute (or just placed in the wrapper). The
script replaces the native suggestions with a styled list.
Whatever is typed stays the value, even if it is not in the list.
Without the script the native datalist still works.
<div class="autocomplete" style="width: 100%; max-width: 320px">
<input class="input" name="city" placeholder="City" aria-label="City" list="ac-cities" />
<datalist id="ac-cities">
<option value="Amsterdam"></option>
<option value="Athens"></option>
<option value="Bangkok"></option>
<option value="Barcelona"></option>
<option value="Berlin"></option>
<option value="Brussels"></option>
<option value="Budapest"></option>
<option value="Cairo"></option>
<option value="Copenhagen"></option>
<option value="Dublin"></option>
<option value="Lisbon"></option>
<option value="London"></option>
<option value="Madrid"></option>
<option value="Paris"></option>
<option value="Prague"></option>
<option value="Rome"></option>
<option value="Vienna"></option>
</datalist>
</div>
In a field with a label and a hint
The label points at the input as usual. Arrow keys move through the suggestions and Enter picks one. With no active suggestion Enter keeps the typed text.
<div class="field" style="width: 100%; max-width: 320px">
<label for="ac-field">Home city</label>
<div class="autocomplete">
<input class="input" id="ac-field" name="home" list="ac-field-list" aria-describedby="ac-field-hint" />
<datalist id="ac-field-list">
<option value="Amsterdam"></option>
<option value="Athens"></option>
<option value="Bangkok"></option>
<option value="Barcelona"></option>
<option value="Berlin"></option>
<option value="Brussels"></option>
<option value="Budapest"></option>
<option value="Cairo"></option>
<option value="Copenhagen"></option>
<option value="Dublin"></option>
<option value="Lisbon"></option>
<option value="London"></option>
<option value="Madrid"></option>
<option value="Paris"></option>
<option value="Prague"></option>
<option value="Rome"></option>
<option value="Vienna"></option>
</datalist>
</div>
<small id="ac-field-hint">Pick a suggestion or type any place.</small>
</div>
Show all suggestions on focus: data-min-length="0"
By default the list opens after one character
(data-min-length="1"). Set it to 0 to show the
suggestions as soon as the input is focused or clicked.
data-limit caps how many are shown (default 8).
<div class="autocomplete" data-min-length="0" data-limit="5" style="width: 100%; max-width: 320px">
<input class="input" name="city0" placeholder="Focus to see suggestions" aria-label="City" list="ac-all" />
<datalist id="ac-all">
<option value="Amsterdam"></option>
<option value="Athens"></option>
<option value="Bangkok"></option>
<option value="Barcelona"></option>
<option value="Berlin"></option>
<option value="Brussels"></option>
<option value="Budapest"></option>
<option value="Cairo"></option>
<option value="Copenhagen"></option>
<option value="Dublin"></option>
<option value="Lisbon"></option>
<option value="London"></option>
<option value="Madrid"></option>
<option value="Paris"></option>
<option value="Prague"></option>
<option value="Rome"></option>
<option value="Vienna"></option>
</datalist>
</div>
Remote suggestions: data-url
data-url on the wrapper is the search URL. The script
sends GET url?q=typed (200 ms after the last key),
aborts the previous request and caches the reply for each query.
The reply is a JSON array of strings or of
{ value, label, text } objects. This page reads the
static file data/cities.json, which ignores
q, so the script also filters the list by the typed
text. While loading, the input and the list have
aria-busy.
<div class="autocomplete" data-url="data/cities.json" style="width: 100%; max-width: 320px">
<input class="input" name="city-remote" placeholder="City (from the server)" aria-label="City" />
</div>
Suggestions with a label and a line of text
A datalist option can carry a label (shown in the
list) and data-text (a muted line under it). The
value is what goes into the input. The matched part
of the label is wrapped in <mark>. Text is
searched too.
<div class="autocomplete" style="width: 100%; max-width: 320px">
<input class="input" name="product" placeholder="Product title" aria-label="Product title" list="ac-products" />
<datalist id="ac-products">
<option value="Canvas tote bag" data-text="Bags, 24 in stock"></option>
<option value="Canvas backpack" data-text="Bags, 8 in stock"></option>
<option value="Cotton tee" data-text="Clothing, 120 in stock"></option>
<option value="Cotton hoodie" data-text="Clothing, 9 in stock"></option>
<option value="Ceramic mug" data-text="Home, 52 in stock"></option>
<option value="Ceramic planter" data-text="Home, 14 in stock"></option>
</datalist>
</div>
A custom source function
For any other lookup call
tsunami.autocomplete.mount(el, { source }). The
function gets the typed text and { signal } and
returns (or resolves to) an array of strings or items. Here it
completes an email domain.
<div class="autocomplete" id="ac-email" data-min-length="1" style="width: 100%; max-width: 320px">
<input class="input" type="email" name="email" placeholder="you@example.com" aria-label="Email" />
</div>
<script type="module">
const domains = ["gmail.com", "outlook.com", "icloud.com", "example.org"];
tsunami.autocomplete.mount(document.getElementById("ac-email"), {
source: async (q) => {
const [name, domain = ""] = q.split("@");
if (!q.includes("@")) return [];
return domains
.filter((d) => d.startsWith(domain))
.map((d) => ({ value: `${name}@${d}`, label: `${name}@${d}` }));
},
});
</script>
Inside an input group, with an icon
Put an .input-group inside the wrapper. The list
lines up with the whole box. data-size on the input
(or the group) sets the height as usual.
<div class="autocomplete" style="width: 100%; max-width: 320px">
<div class="input-group">
<i class="icon icon-map-pin" aria-hidden="true"></i>
<input name="place" placeholder="Where to?" aria-label="Where to?" list="ac-group" />
</div>
<datalist id="ac-group">
<option value="Amsterdam"></option>
<option value="Athens"></option>
<option value="Bangkok"></option>
<option value="Barcelona"></option>
<option value="Berlin"></option>
<option value="Brussels"></option>
<option value="Budapest"></option>
<option value="Cairo"></option>
<option value="Copenhagen"></option>
<option value="Dublin"></option>
<option value="Lisbon"></option>
<option value="London"></option>
<option value="Madrid"></option>
<option value="Paris"></option>
<option value="Prague"></option>
<option value="Rome"></option>
<option value="Vienna"></option>
</datalist>
</div>
Disabled and read-only
A disabled or read-only input shows no suggestions.
<div class="stack" style="width: 100%; max-width: 320px">
<div class="autocomplete">
<input class="input" placeholder="Disabled" aria-label="Disabled" list="ac-off" disabled />
<datalist id="ac-off">
<option value="Amsterdam"></option>
<option value="Athens"></option>
<option value="Bangkok"></option>
<option value="Barcelona"></option>
<option value="Berlin"></option>
<option value="Brussels"></option>
<option value="Budapest"></option>
<option value="Cairo"></option>
<option value="Copenhagen"></option>
<option value="Dublin"></option>
<option value="Lisbon"></option>
<option value="London"></option>
<option value="Madrid"></option>
<option value="Paris"></option>
<option value="Prague"></option>
<option value="Rome"></option>
<option value="Vienna"></option>
</datalist>
</div>
<div class="autocomplete">
<input class="input" value="Paris" aria-label="Read-only" list="ac-ro" readonly />
<datalist id="ac-ro">
<option value="Amsterdam"></option>
<option value="Athens"></option>
<option value="Bangkok"></option>
<option value="Barcelona"></option>
<option value="Berlin"></option>
<option value="Brussels"></option>
<option value="Budapest"></option>
<option value="Cairo"></option>
<option value="Copenhagen"></option>
<option value="Dublin"></option>
<option value="Lisbon"></option>
<option value="London"></option>
<option value="Madrid"></option>
<option value="Paris"></option>
<option value="Prague"></option>
<option value="Rome"></option>
<option value="Vienna"></option>
</datalist>
</div>
</div>
Autocomplete or Select?
Use Select when the value must be one of a
fixed set of options (even with data-input or
data-tags, the choice is an option of a native
<select>). Use Autocomplete when the value is
free text and the suggestions only save typing: a city, a product
title, an email domain, a search box.
API
The script enhances every .autocomplete on load and
when added later. The functions are only needed for a custom
source.
| Function | Arguments | Description |
|---|---|---|
tsunami.autocomplete.mount(el, options)
|
el: the .autocomplete wrapper,
options.source:
async (q, { signal }) => items
|
Enhances the wrapper with a source function, replacing a
previous setup. Items are strings or
{ value, label, text }. Replies are cached
for each query and a new call aborts the previous one
through signal.
|
tsunami.autocomplete.destroy(el) |
el: the wrapper |
Removes the list and the ARIA attributes, restores the
input's list attribute and stops enhancing
this wrapper.
|
Events
| Event | On | Detail |
|---|---|---|
tsunami:autocomplete |
input, bubbles |
{ value, item }, where item is
{ value, label, text }. Fired when a
suggestion is picked, not for typed text.
|
input, change |
input, bubbles | Fired after a pick, so forms and the Save bar react as if the text had been typed. Typing fires the native events as usual. |
Keyboard
| Key | Description |
|---|---|
| ↓ ↑ |
Open the list if it is closed. In the list: move the
active suggestion (aria-activedescendant).
|
| Enter | Picks the active suggestion. With none active it keeps the typed text (and submits the form as usual). |
| Escape | Closes the list. The text stays. |
| Tab | Leaves the input, closes the list and keeps the text. |
| Letters | Filter the suggestions. Any text is a valid value. |
Attributes
| Attribute | Value | Description |
|---|---|---|
data-url |
URL |
On the wrapper. Remote suggestions:
GET url?q=, JSON array of strings or
{ value, label, text }.
|
data-min-length |
number (default 1) | On the wrapper. Characters needed before the list opens. 0 shows all on focus. |
data-limit |
number (default 8) | On the wrapper. Most suggestions shown. |
list |
datalist id |
On the input. The script removes it (the datalist stays
the data source) and puts it back on destroy.
A datalist inside the wrapper works too.
|
option[value] |
text | In the datalist. The text put into the input. |
option[label] |
text | Shown in the list instead of the value. Matched and highlighted. |
option[data-text] |
text | A muted line under the title. Searched, not highlighted. |
data-size |
sm, lg | On the input, as for Input. The list follows the width of the input (or the input group). |
aria-busy |
set by the script | On the input and the listbox while remote suggestions load. |
Accessibility
The input becomes an ARIA 1.2 combobox with list autocomplete:
role="combobox",
aria-autocomplete="list", aria-expanded,
aria-controls (the listbox) and
aria-activedescendant (the active option). The list
is a role="listbox" labelled by the input's
<label> or aria-label, with a
role="option" for each suggestion. Focus never leaves
the input. Give every input a label.