On this page Autocomplete Autocomplete

The m3e-autocomplete component enhances an input field with a panel of suggested options.

import "@m3e/web/autocomplete";
Usage

This section outlines usage examples and configuration guidance for the components in this package.

Basic usage

Begin by declaring the m3e-autocomplete panel and populating it with a set of m3e-option elements. Each option represents a selectable value. When a user selects an option, its textual content becomes the value of the associated text input. Alternatively, you may specify a distinct value using the value attribute.

To bind the autocomplete panel to an input, use the for attribute on m3e-autocomplete, setting it to match the id of the target input element.

The following example illustrates use of a m3e-autocomplete in conjunction with the m3e-form-field. See Form Field for more information.

Apples Oranges Bananas Grapes
<m3e-form-field>
  <label slot="label" for="fruit">Choose your favorite fruit</label>
  <input id="fruit" />
</m3e-form-field>
<m3e-autocomplete for="fruit">
  <m3e-option>Apples</m3e-option>
  <m3e-option>Oranges</m3e-option>
  <m3e-option>Bananas</m3e-option>
  <m3e-option>Grapes</m3e-option>
</m3e-autocomplete>
Filter modes

By default, m3e-autocomplete will filter options using a case insensitive substring match. You can use the filter attribute to control this behavior. Supported values include: contains (default), starts-with, ends-with and none. In addition, use the case-sensitive attribute to control whether matching is case-sensitive.

The following example illustrates a case-sensitive prefix match.

Apples Oranges Bananas Grapes
<m3e-form-field>
  <label slot="label" for="fruit">Choose your favorite fruit</label>
  <input id="fruit" value="Apple" />
</m3e-form-field>
<m3e-autocomplete for="fruit" filter="starts-with" case-sensitive>
  <!-- Options omitted for brevity -->
</m3e-autocomplete>
Custom filtering

Alternately, you can provide a callback function using the filter property to control this behavior. Custom filters do not support automatic text highlighting, because the component cannot determine which parts of the text matched.

The following example shows how to set filter with logic for a custom case-insensitive prefix match.

Apples Oranges Bananas Grapes
<m3e-form-field>
  <label slot="label" for="fruit">Choose your favorite fruit</label>
  <input id="fruit" value="Apple" />
</m3e-form-field>
<m3e-autocomplete class="custom-filter" for="fruit">
  <!-- Options omitted for brevity -->
</m3e-autocomplete>
window.addEventListener("DOMContentLoaded", () => {
  for (const autocomplete of document.querySelectorAll("m3e-autocomplete.custom-filter")) {
    autocomplete.filter = (option, term) =>
      option.value.toLocaleLowerCase().startsWith(term.toLocaleLowerCase());
  }
});
No data

Use the no-data-label attribute to control the text announced and displayed when no options match. The no-data slot can be used to provide custom visual content for the empty state. Alternatively, use the hide-no-data attribute to suppress the empty-state UI entirely.

Apples Oranges Bananas Grapes
<m3e-form-field>
  <label slot="label" for="fruit">Choose your favorite fruit</label>
  <input id="fruit" value="Pear" />
</m3e-form-field>
<m3e-autocomplete for="fruit" no-data-label="No data">
  <!-- Options omitted for brevity -->
</m3e-autocomplete>
Lazy loading

The m3e-autocomplete emits a query event that can be used to lazy-load options. The event fires only when the control is enabled and not read-only, under the following conditions:

Use the loading attribute to control whether the m3e-autocomplete presents a loading UI. The loading-label attribute controls the text announced and displayed when enabled and the loading slot can be used to provide custom visual content for the loading state. Use the hide-loading attribute to suppress the loading-state UI entirely. loading-label will still be announced when hidden.

Initial load

The following example illustrates lazy loading initial data when the input is focused. In addition, a m3e-loading-indicator is presented in the loading slot. See Loading Indicator for more information.

<m3e-form-field>
  <label slot="label" for="state">State</label>
  <input id="state" />
</m3e-form-field>
<m3e-autocomplete for="state" class="lazy">
</m3e-autocomplete>
const lazy = document.querySelector("m3e-autocomplete.lazy");
lazy.addEventListener("query", () => {
  if (!lazy.querySelector("m3e-option")) {
    lazy.loading = true;
    setTimeout(() => {
      usStates.forEach((state) => {
        const option = document.createElement("m3e-option");
        option.innerText = state;
        lazy.appendChild(option);
      });
      lazy.loading = false;
    }, 2000);
  }
});
Search as you type

The next example performs a search only after the user pauses typing. The loading indicator is not shown while the user is typing and appears only when an actual search request is made. If the user begins typing while a search is in progress, the loading indicator remains visible, the in-flight request is canceled, and a new search is started after debouncing. This example sets filter="none" to disable built-in filtering.

<m3e-form-field>
  <label slot="label" for="state">State</label>
  <input id="state" />
</m3e-form-field>
<m3e-autocomplete for="state" filter="none" class="search">
</m3e-autocomplete>
const search = document.querySelector("m3e-autocomplete.search");

let debounceTimer = -1;
let activeRequest = null;

search.addEventListener("query", (e) => {
  const term = e.detail.term?.trim().toLowerCase();

  // User is typing → reset debounce, but do NOT show loading yet
  clearTimeout(debounceTimer);

  // If user cleared the field
  if (!term) {
    // Cancel any in‑flight request
    if (activeRequest) activeRequest.abort();

    // Reset UI
    search.querySelectorAll("m3e-option").forEach((x) => x.remove());
    search.hideNoData = true;
    search.loading = false;
    return;
  }

  // Now a real search is happening → show loading
  search.loading = true;
  search.hideNoData = false;

  // If a request is already loading, keep showing loading
  // but cancel the old request
  if (activeRequest) activeRequest.abort();

  // Start debounce
  debounceTimer = setTimeout(() => {
    // Clear old results (so we don't show stale data)
    search.querySelectorAll("m3e-option").forEach((x) => x.remove());

    // Create a cancellable "request"
    const controller = new AbortController();
    activeRequest = controller;

    // Simulate async search
    setTimeout(() => {
      // If canceled, do nothing
      if (controller.signal.aborted) return;

      // Populate results
      usStates
        .filter((x) => x.toLowerCase().includes(term))
        .forEach((state) => {
          const option = document.createElement("m3e-option");
          option.innerText = state;
          search.appendChild(option);
        });

      search.loading = false;

      activeRequest = null;
    }, 2000);
  }, 300); // debounce delay
});
Requiring an option to be selected

By default, m3e-autocomplete accepts any value typed into the input field, regardless of whether it matches a listed option. To enforce selection from the available options, use the required attribute.

When required is set, the autocomplete behaves as follows:

Apples Oranges Bananas Grapes
<m3e-form-field>
  <label slot="label" for="fruit">Choose your favorite fruit</label>
  <input id="fruit" value="Apple" />
</m3e-form-field>
<m3e-autocomplete for="fruit" required>
  <!-- Options omitted for brevity -->
</m3e-autocomplete>
Automatic activation

Use the auto-activate attribute to control whether the first option will automatically be activated.

Apples Oranges Bananas Grapes
<m3e-form-field>
  <label slot="label" for="fruit">Choose your favorite fruit</label>
  <input id="fruit" />
</m3e-form-field>
<m3e-autocomplete for="fruit" auto-activate>
  <!-- Options omitted for brevity -->
</m3e-autocomplete>
Chips

The m3e-autocomplete pairs with m3e-input-chip-set to surface suggestions that users can select and convert into chips. See Chips for more information.

Apples Oranges Bananas Grapes
<m3e-form-field>
  <label slot="label" for="fruit">Choose your favorite fruits</label>
  <m3e-input-chip-set aria-label="Enter favorite fruits">
    <input id="fruit" slot="input" placeholder="Add fruit..." />
  </m3e-input-chip-set>
</m3e-form-field>
<m3e-autocomplete for="fruit">
  <!-- Options omitted for brevity -->
</m3e-autocomplete>
Accessibility

The m3e-autocomplete component follows the ARIA combobox interaction pattern. The associated text input receives role="combobox", while the dropdown panel applies role="listbox" to convey its structure to assistive technologies.

Options are given ARIA role="option", indicating that each item represents a selectable choice within a listbox context. This role enables assistive technologies to interpret and announce the options appropriately, supporting accessible navigation and selection behavior.

The aria-selected attribute reflects whether an option is currently selected. When an option is selected, aria-selected="true" is exposed to assistive technologies; otherwise, it is "false".

When disabled using the disabled attribute, aria-disabled="true" is used to convey to assistive technologies that an option is disabled.

The aria-activedescendant attribute is applied to the input element to indicate which option is currently active within the listbox. Instead of moving DOM focus to each option, the select retains focus while updating aria-activedescendant to reference the id of the focused m3e-option. This approach preserves focus management and enables assistive technologies to announce the active option during keyboard navigation, ensuring accessible and predictable interaction.

The aria-owns and aria-controls attributes are applied to the input element to establish an explicit relationship with the listbox popup. Both attributes reference the id of the listbox container, ensuring assistive technologies recognize the connection between the trigger and the controlled content. While aria-controls indicates that the select governs the visibility and behavior of the listbox, aria-owns asserts DOM ownership when the listbox is rendered outside the input's subtree.

The aria-expanded attribute reflects the current state of the listbox popup. When the listbox is open, aria-expanded="true" is set on the input element; when collapsed, it is set to false. This dynamic state enables screen readers to announce whether the listbox is expanded or collapsed, supporting accessible navigation and interaction.

Because listbox is designed for single-item selection, you should avoid placing additional interactive elements—such as buttons, checkboxes, or toggles—inside m3e-option. Nesting interactive controls within options disrupts expected navigation and interferes with screen reader behavior.

The component provides built-in ARIA-friendly announcements for loading, no-data, and results updates. These announcements are exposed through a polite aria-live region so assistive technologies receive timely, non-interruptive updates as the user types.

Native module support

The @m3e/web package uses JavaScript Modules. To use it directly in a browser without a bundler, use a module script similar to the following.

<script type="module" src="/node_modules/@m3e/web/dist/autocomplete.js"></script>

You also need a module script for @m3e/option due to it being a dependency.

<script type="module" src="/node_modules/@m3e/web/dist/option.js"></script>

In addition, you must use an import map to include additional dependencies.

<script type="importmap">
  {
    "imports": {
      "tslib": "https://cdn.jsdelivr.net/npm/tslib@2.8.1/+esm",
      "lit": "https://cdn.jsdelivr.net/npm/lit@3.3.0/+esm",
      "lit/": "https://cdn.jsdelivr.net/npm/lit@3.3.0/",
      "lit-html": "https://cdn.jsdelivr.net/npm/lit-html@3.3.0/+esm",
      "lit-html/directive.js": "https://cdn.jsdelivr.net/npm/lit-html@3.3.0/directive.js",
      "lit-html/directives/if-defined.js": "https://cdn.jsdelivr.net/npm/lit-html@3.3.0/directives/if-defined.js",
      "lit-html/directives/class-map.js": "https://cdn.jsdelivr.net/npm/lit-html@3.3.0/directives/class-map.js",
      "@lit/reactive-element": "https://cdn.jsdelivr.net/npm/@lit/reactive-element@2.0.4/+esm",
      "@lit/reactive-element/": "https://cdn.jsdelivr.net/npm/@lit/reactive-element@2.0.4/",
      "@m3e/web/core": "/node_modules/@m3e/web/dist/core.js",
      "@m3e/web/core/a11y": "/node_modules/@m3e/web/dist/core-a11y.js",
      "@m3e/web/core/anchoring": "/node_modules/@m3e/web/dist/core-anchoring.js",
      "@m3e/web/core/bidi": "/node_modules/@m3e/web/dist/core-bidi.js",
      "@m3e/web/option": "/node_modules/@m3e/web/dist/option.js"
    }
  }
</script>

For production builds, use the minified files to ensure optimal load performance.

API

The @m3e/web package includes a Custom Elements Manifest (custom-elements.json), which documents the properties, attributes, slots, events and CSS custom properties of each component.

You can explore the API below, or integrate the manifest into your own tooling.