Skip to content

ExpressiveCSS

Browse documentation

Autocomplete

Suggest values under a text field as the user types.

View as Markdown

Autocomplete keeps the original suggestion dataset and the selected display label while the user edits. Escape and Tab in the input close the menu and cancel a pending open. Listbox options stay out of the sequential Tab order.

isOpen follows the suggestion menu through rerenders and dismissal. When the selection changes, the input emits a bubbling, composed change event, so delegated form listeners can observe it.

Autocomplete adds a visually hidden, polite live region for loading, result counts and multi-selection changes. The visible spinner and count stay unchanged. It announces only while the input has focus and its text meets minLength. Closing the menu clears the region. Check spoken delivery with a screen reader.

Override i18n.loading and i18n.noResults, which default to "Loading results." and "No results.". For localized plurals, supply i18n.results(count) and i18n.selected(count) functions. The defaults produce "1 result available." or "N results available." and "1 item selected." or "N items selected.". i18n.create(text) labels the "Add" row and defaults to Add "text". Missing keys keep their defaults. All messages are plain text.

When a search returns no suggestions, the open list also shows i18n.noResults as its first row, so sighted users see what the live region says. The row is not an option. It is hidden from assistive technology, arrow keys skip it, and a click on it chooses nothing and leaves focus in the field. The row goes away when a search returns a match. In Chips, pressing Enter to add the typed text as a chip closes the list.

Updates announce the count passed to setMenuItems, excluding fallback selected items, disabled entries and the "Add" row. Opening an existing list announces its displayed item count, without disabled entries. Updates with open: false stay silent. Your application handles request cancellation, stale-response ordering and errors. The plugin ignores results delivered after destroy(), and results delivered after the user leaves the field update the list without opening it.

Add autocomplete to a text input inside .field. AutoInit() starts every .autocomplete except those with no-autoinit. The default data list is empty, so pass options, or call init, to give it something to suggest.

Set isMultiSelect: true to pick several values. A count appears on the field.

Set disabled: true on an entry to show it without letting anyone choose it. Oslo is unavailable, and Vienna is selected and cannot be removed.

Set group on entries to list them under headings.

<div class="field">
  <span class="material-symbols prefix" aria-hidden="true">textsms</span>
  <input type="text" id="autocomplete-input" class="autocomplete" placeholder=" ">
  <label for="autocomplete-input">Autocomplete</label>
</div>

Initialization

The IIFE bundle exposes Expressive.Autocomplete. data is an array of objects:

  • id (required). A string or number, used as the option text when text is omitted.
  • text. The display label. The default search matches id and text.
  • image. An image URL, shown as a circle. The default search ignores it.
  • description. An optional secondary line under the label.
  • disabled. true keeps the entry in the list, dimmed, but nobody can choose it.
  • group. An optional heading to list the entry under.

The default search ignores extra properties. Filter them yourself in onSearch.

document.addEventListener('DOMContentLoaded', function() {
  const elems = document.querySelectorAll('.autocomplete');
  Expressive.Autocomplete.init(elems, {
    minLength: 0,
    data: [
      { id: 12, text: 'Apple' },
      { id: 13, text: 'Microsoft' },
      { id: 42, text: 'Google', image: 'https://picsum.photos/id/64/250/250' }
    ]
  });
});

Multiple selection:

Expressive.Autocomplete.init(document.querySelector('#autocomplete-multi'), {
  minLength: 0,
  isMultiSelect: true,
  data: [
    { id: 12, text: 'Apple' },
    { id: 13, text: 'Microsoft' },
    { id: 42, text: 'Google', image: 'https://picsum.photos/id/64/250/250' }
  ]
});

A disabled entry stays in the results so people can see it exists. A click, Enter and selectOption() all leave it as it is, so no change or expressive:autocomplete fires. Up and Down skip it, and the announced result count leaves it out, so a list of only disabled matches announces i18n.noResults. The selected option and setMenuItems(items, selected) can still select one, which shows a choice the person cannot remove:

Expressive.Autocomplete.init(document.querySelector('#autocomplete-disabled'), {
  minLength: 0,
  isMultiSelect: true,
  selected: ['vienna'],
  data: [
    { id: 'lisbon', text: 'Lisbon' },
    { id: 'oslo', text: 'Oslo', disabled: true },
    { id: 'rome', text: 'Rome' },
    { id: 'vienna', text: 'Vienna', disabled: true }
  ]
});

Give entries a group to list them under labelled headings. Entries with the same group gather where the group first appears in the results, so sort them yourself if the order matters; entries without one stay outside every group. Up and Down pass over a heading to the next entry, the announced result count leaves headings out, and a click on a heading chooses nothing. The heading is plain text, even with allowUnsafeHTML:

Expressive.Autocomplete.init(document.querySelector('#autocomplete-groups'), {
  minLength: 0,
  data: [
    { id: 'lisbon', text: 'Lisbon', group: 'Portugal' },
    { id: 'porto', text: 'Porto', group: 'Portugal' },
    { id: 'bergen', text: 'Bergen', group: 'Norway' },
    { id: 'oslo', text: 'Oslo', group: 'Norway' }
  ]
});

A custom onSearch can load data asynchronously. When the list is ready, call setMenuItems. The default filter looks at id and text only:

onSearch: function(text, autocomplete) {
  const normSearch = text.toLocaleLowerCase();
  autocomplete.setMenuItems(
    autocomplete.options.data.filter(function(option) {
      return option.id.toString().toLocaleLowerCase().includes(normSearch)
        || (option.text && option.text.toLocaleLowerCase().includes(normSearch));
    })
  );
}

The third argument is an AbortSignal. It aborts when the search text changes or the autocomplete is destroyed. Pass it to fetch() so a slow response for old text never replaces the results for the current text. Clear the list on any other error. Set searchDelay so the request waits for typing to pause instead of going out on every key:

searchDelay: 300,
onSearch: function(text, autocomplete, signal) {
  fetch('/api/search?q=' + encodeURIComponent(text), { signal })
    .then(function(response) { return response.json(); })
    .then(function(items) { autocomplete.setMenuItems(items); })
    .catch(function(error) {
      // A newer search aborted this one; its results are stale.
      if (error.name !== 'AbortError') autocomplete.setMenuItems([]);
    });
}

Set allowCreate to offer the typed text as a new value. The list then ends in an "Add" row whenever the trimmed text matches no listed suggestion. The comparison ignores case and checks each suggestion's text, or its id when it has no text. Choosing the row with Enter or a click calls onCreate with the trimmed text and fires expressive:create with { text }. The selection does not change, so add and select the entry yourself:

allowCreate: true,
onCreate: function(text, autocomplete) {
  const entry = { id: text, text: text };
  autocomplete.options.data.push(entry);
  autocomplete.setMenuItems(autocomplete.options.data, [entry.id], false);
}

Options

Name Type Default Description
data Array [] Suggestion list. Each item needs an id. The text, image, and description fields are optional.
isMultiSelect Boolean false If true, the user can select several values. onAutocomplete receives an array.
maxMenuHeight String '300px' Max height of the suggestion menu.
onAutocomplete Function null Called after a selection and when a default value is applied. Receives the selected entries.
onSearch Function filters id and text Called with the text, the instance and an AbortSignal when input events change the search text, including paste, replacement and deletion without key events. Load or filter data, then call setMenuItems. The signal aborts when the search text changes or the autocomplete is destroyed.
allowCreate Boolean false If true, text that matches no listed suggestion exactly ends the list in an "Add" row. Choosing it calls onCreate and fires expressive:create. The selection does not change.
onCreate Function null Called with the trimmed text and the instance when the "Add" row is chosen.
searchDelay Number 0 Milliseconds to wait after typing pauses before calling onSearch, so remote data is fetched once per pause. A search on focus runs at once. Blur, destroy() and choosing a single result cancel a pending search. 0 searches on every input.
minLength Number 1 Characters required before suggestions open. 0 shows the list on click or focus.
menuOptions Object see note Options for Menu. Defaults include autoFocus: false, closeOnClick: false, and coverTrigger: false.
allowUnsafeHTML Boolean false If true, each data entry's text is inserted as HTML. Ids always stay text. Only use sanitized data.
selected Array [] Initial selected ids (strings or numbers). An id without loaded data displays the id. Pass the selected ids to setMenuItems with the loaded data to resolve their labels.

Methods

All methods are called on the plugin instance. You can get the instance like this:
const instance = Expressive.Autocomplete.getInstance(elem);
.open();

Open the suggestion menu if the input meets minLength.

instance.open();
.close();

Close the suggestion menu.

instance.close();
.selectOption();

Select (or toggle, when multi-select) the entry with this id.

instance.selectOption(42);
.setMenuItems();

Replace the visible suggestions. Optionally pass selected ids and whether to open the menu (default true).

instance.setMenuItems([
  { id: 'Test' },
  { id: 12, text: 'Apple' },
  { id: 13, text: 'Microsoft' },
  { id: 42, text: 'Google', image: 'https://picsum.photos/id/64/250/250' }
]);
.destroy();

Destroy the plugin instance, remove the menu, and detach its event handlers.

instance.destroy();

Properties

Name Type Description
el Element The input the plugin was initialized with.
options Object The options the instance was initialized with.
isOpen Boolean Whether the suggestion menu is open.
count Number Number of matching options (reset on each input or focus).
activeIndex Number Index of the keyboard-highlighted option, or -1.
menu Menu The Menu instance for this autocomplete.
selectedValues Array The currently selected entries.

No matching pages

  • Source color

    The seed every generated ramp derives from. Pick one and browse the docs — the whole theme follows. Error does not: it is a fixed hue.

    Default source: Mist Iris.

    ExpressiveCSS presets: Lagoon, Harbor, Breeze, Cobalt, Iris.

    Bold

    Soft

    Mist

    Glow