GitHub

Autocomplete

The Autocomplete component combines a text input with a listbox popover, letting users filter a list of options by typing. It follows the WAI-ARIA combobox pattern and supports static lists, custom filtering, and options loaded asynchronously from an API. Like Select, it is built on top of the Listbox and Popover components and renders a hidden native <select> for form submission.

Use Autocomplete when the list is long enough that typing beats scrolling — assigning a country, a time zone, a teammate. Use Select when scanning a short list is faster than typing, or when you need multiple selection (Select supports @selectionMode="multiple" together with @isFilterable); Autocomplete is single-selection only.

Import

import { Autocomplete } from 'frontile';

Usage

Basic Autocomplete

Type into the input to filter the options. The default filter is a case-insensitive "contains" match.

import { Autocomplete } from 'frontile';

const countries = [
  'Argentina',
  'Australia',
  'Brazil',
  'Canada',
  'Denmark',
  'France',
  'Germany',
  'Japan',
  'Mexico',
  'Netherlands',
  'New Zealand',
  'Portugal',
  'South Korea',
  'Spain',
  'United Kingdom',
  'United States'
];

<template>
  <Autocomplete @placeholder='Search countries' @items={{countries}} />
</template>

Selection

Pass @selectedKey and update it in @onSelectionChange to maintain two-way binding, the same data flow as Select.

Selected: none

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Autocomplete } from 'frontile';

const languages = [
  'Elixir',
  'Go',
  'JavaScript',
  'Python',
  'Ruby',
  'Rust',
  'TypeScript'
];

export default class LanguagePicker extends Component {
  @tracked selectedKey: string | null = null;

  onSelectionChange = (key: string | null) => {
    this.selectedKey = key;
  };

  <template>
    <Autocomplete
      @label='Primary language'
      @placeholder='Search languages'
      @items={{languages}}
      @selectedKey={{this.selectedKey}}
      @onSelectionChange={{this.onSelectionChange}}
    />
    <p class='mt-4'>Selected: {{if this.selectedKey this.selectedKey 'none'}}</p>
  </template>
}

Async search with an API

Pass @onSearch to load options from an API as the user types. The component debounces calls (250ms by default, tune with @searchDebounce), shows a loading spinner while the returned promise is pending, and ignores stale responses so the latest query always wins. Clearing the input restores @items without triggering a search.

This example simulates a request with network latency:

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Autocomplete } from 'frontile';

interface City {
  key: string;
  label: string;
}

const cities: City[] = [
  { key: 'amsterdam', label: 'Amsterdam' },
  { key: 'barcelona', label: 'Barcelona' },
  { key: 'berlin', label: 'Berlin' },
  { key: 'buenos-aires', label: 'Buenos Aires' },
  { key: 'lisbon', label: 'Lisbon' },
  { key: 'london', label: 'London' },
  { key: 'melbourne', label: 'Melbourne' },
  { key: 'mexico-city', label: 'Mexico City' },
  { key: 'new-york', label: 'New York' },
  { key: 'sao-paulo', label: 'São Paulo' },
  { key: 'seoul', label: 'Seoul' },
  { key: 'tokyo', label: 'Tokyo' }
];

// Stand-in for a real API call, e.g. fetch(`/api/cities?q=${query}`)
const searchCities = (query: string): Promise<City[]> => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve(
        cities.filter((city) =>
          city.label.toLowerCase().includes(query.toLowerCase())
        )
      );
    }, 600);
  });
};

export default class CitySearch extends Component {
  @tracked selectedKey: string | null = null;

  onSelectionChange = (key: string | null) => {
    this.selectedKey = key;
  };

  <template>
    <Autocomplete
      @label='Destination'
      @placeholder='Search cities'
      @searchMessage='Type to search for a city...'
      @onSearch={{searchCities}}
      @selectedKey={{this.selectedKey}}
      @onSelectionChange={{this.onSelectionChange}}
    >
      <:emptyContent>No cities match your search.</:emptyContent>
    </Autocomplete>
  </template>
}

Pass @searchMessage (or a :searchMessage block for rich content) to prompt users who open the dropdown before typing — it shows while the query is blank and there are no options to display, as in the example above. Without it, an opened async autocomplete with no default @items shows the empty content instead.

External filtering

If you want full control over filtering — for example, filtering server-side while controlling the request lifecycle yourself — pass @disableFiltering={{true}} and update @items from @onInputChange:

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Autocomplete } from 'frontile';

const allTimezones = [
  'America/Chicago',
  'America/Denver',
  'America/Los_Angeles',
  'America/New_York',
  'Asia/Seoul',
  'Asia/Tokyo',
  'Australia/Sydney',
  'Europe/Berlin',
  'Europe/Lisbon',
  'Europe/London'
];

export default class TimezonePicker extends Component {
  @tracked items = allTimezones;

  onInputChange = (value: string) => {
    // Replace with your own request/filter logic
    this.items = allTimezones.filter((tz) =>
      tz.toLowerCase().includes(value.toLowerCase())
    );
  };

  <template>
    <Autocomplete
      @label='Time zone'
      @placeholder='Search time zones'
      @items={{this.items}}
      @disableFiltering={{true}}
      @onInputChange={{this.onInputChange}}
    />
  </template>
}

Custom filter

Pass @filter to change how items match the typed text — here, matching only from the start of the word:

import { Autocomplete } from 'frontile';

const fruits = ['Apple', 'Apricot', 'Banana', 'Cherry', 'Grape', 'Pineapple'];

const startsWith = (itemValue: string, inputValue: string) =>
  itemValue.toLowerCase().startsWith(inputValue.toLowerCase());

<template>
  <Autocomplete
    @placeholder="Try typing 'ap'"
    @items={{fruits}}
    @filter={{startsWith}}
  />
</template>

Custom items

Use the :item block to render richer options. Objects with key and label properties work out of the box.

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Autocomplete } from 'frontile';

const teammates = [
  { key: 'ana', label: 'Ana Souza', role: 'Design' },
  { key: 'devon', label: 'Devon Lane', role: 'Engineering' },
  { key: 'kim', label: 'Kim Park', role: 'Product' },
  { key: 'marta', label: 'Marta Silva', role: 'Engineering' },
  { key: 'ravi', label: 'Ravi Patel', role: 'Support' }
];

export default class AssigneePicker extends Component {
  @tracked selectedKey: string | null = null;

  onSelectionChange = (key: string | null) => {
    this.selectedKey = key;
  };

  <template>
    <Autocomplete
      @label='Assignee'
      @placeholder='Search teammates'
      @items={{teammates}}
      @selectedKey={{this.selectedKey}}
      @onSelectionChange={{this.onSelectionChange}}
    >
      <:item as |l|>
        <l.Item @key={{l.key}} @description={{l.item.role}}>
          {{l.label}}
        </l.Item>
      </:item>
    </Autocomplete>
  </template>
}

Custom values

By default the input reverts to the selected option's label when the dropdown closes. Pass @allowsCustomValue={{true}} to keep whatever the user typed — useful when suggestions are helpful but not required. Read the final text via @onInputChange.

Pick a suggestion or type your own

Value:

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Autocomplete } from 'frontile';

const commonRoles = ['Admin', 'Editor', 'Viewer'];

export default class RoleInput extends Component {
  @tracked value = '';

  onInputChange = (value: string) => {
    this.value = value;
  };

  <template>
    <Autocomplete
      @label='Role'
      @description='Pick a suggestion or type your own'
      @items={{commonRoles}}
      @allowsCustomValue={{true}}
      @onInputChange={{this.onInputChange}}
    />
    <p class='mt-4'>Value: {{this.value}}</p>
  </template>
}

Clear button

Pass @isClearable={{true}} to show a button that clears both the selection and the typed text.

import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { Autocomplete } from 'frontile';

const browsers = ['Chrome', 'Edge', 'Firefox', 'Safari'];

export default class BrowserPicker extends Component {
  @tracked selectedKey: string | null = 'Firefox';

  onSelectionChange = (key: string | null) => {
    this.selectedKey = key;
  };

  <template>
    <Autocomplete
      @label='Browser'
      @items={{browsers}}
      @isClearable={{true}}
      @selectedKey={{this.selectedKey}}
      @onSelectionChange={{this.onSelectionChange}}
    />
  </template>
}

Sizes and validation

Autocomplete accepts the same form control arguments as other Frontile inputs: @label, @description, @errors, @isInvalid, @isRequired, and @inputSize.

Choose a plan to continue
import { Autocomplete } from 'frontile';
import { array } from '@ember/helper';

const plans = ['Free', 'Pro', 'Enterprise'];

<template>
  <div class='flex flex-col gap-4'>
    <Autocomplete @inputSize='sm' @placeholder='Small' @items={{plans}} />
    <Autocomplete @inputSize='md' @placeholder='Medium' @items={{plans}} />
    <Autocomplete @inputSize='lg' @placeholder='Large' @items={{plans}} />
    <Autocomplete
      @label='Plan'
      @isRequired={{true}}
      @isInvalid={{true}}
      @errors={{array 'Choose a plan to continue'}}
      @items={{plans}}
    />
  </div>
</template>

Keyboard interaction

Key Action
Type characters Opens the dropdown and filters the options
ArrowDown / ArrowUp Opens the dropdown / moves the highlight
Enter Selects the highlighted option
Escape Closes the dropdown
Home / End Moves the highlight to the first / last option

Accessibility

The input uses role="combobox" with aria-autocomplete="list", aria-expanded, and aria-controls pointing at the popover. Focus stays on the input while ArrowUp/ArrowDown move a virtual highlight communicated through aria-activedescendant. A visually hidden native <select> mirrors the options for form submission via @name.

API

Autocomplete

Element: <span class="hljs-title class_">HTMLDivElement</span>

Autocomplete Component - a text input combined with a listbox popover, following the WAI-ARIA 1.2 combobox pattern.

Users filter the options by typing (type-ahead). Options can come from a static list (built-in filtering), an externally filtered list (disableFiltering + onInputChange), or an async source (onSearch).

Arguments

Name Type Default Description
allowEmpty boolean -
allowsCustomValue boolean false When true, the text typed by the user is kept on blur/close even if it does not match any option. By default the input reverts to the selected item's label (or empty) when the dropdown closes without a selection.
appearance enum 'default' The appearance of each item
backdrop enum - How the area behind the overlay is rendered: none omits the backdrop entirely, transparent keeps it clickable but invisible, faded dims the page, and blur blurs it.
backdropTransition Object - Transition classes for the backdrop, overriding the defaults used when it fades in and out.
blockScroll boolean true Whether scrolling should be blocked when the dropdown is open.
classes SlotsToClasses<'base' | 'input' | 'innerContainer' | 'startContent' | 'endContent' | 'listbox' | 'icon' | 'clearButton' | 'emptyContent'> - Custom classes to style different slots within the autocomplete component.
closeOnEscapeKey boolean true Whether to close when the escape key is pressed
closeOnItemSelect boolean true Whether the autocomplete should close upon selecting an item.
closeOnOutsideClick boolean true Whether to close when the area outside (the backdrop) is clicked
description string - Help text rendered between the label and the control, and referenced by the ids describedBy returns.
didClose function - Callback when closing has finished, including any exit transition.
disabledKeys Array -
disableFiltering boolean false Disables the built-in filtering, rendering @items as-is. Use together with inputValue/onInputChange when filtering happens outside of the component (e.g. server-side).
disableFocusTrap boolean true Whether the focus trap should be disabled when the dropdown is open.
disableTransitions boolean false Disable css transitions
endContentPointerEvents enum 'none' Controls pointer-events property of endContent. Defaults to none to pass click events to the input. If your content needs to capture events, add the pointer-events-auto class to that element.
errors enum - Validation messages for the field. A non-empty value also marks the control invalid, and an array is joined with ; when displayed.
filter function - Function to filter the items against the current input text. The default implementation performs a case-insensitive "contains" search.
flipOptions { padding?: Padding; mainAxis?: boolean; crossAxis?: boolean | 'alignment'; fallbackPlacements?: Placement[]; fallbackStrategy?: 'bestFit' | 'initialPlacement'; fallbackAxisSideDirection?: 'none' | ... 1 more ... | 'start'; ... 4 more ...; boundary?: Boundary; } - Options for the floating-ui flip middleware, which moves the content to the opposite side when it would overflow the viewport.
focusTrapOptions any { clickOutsideDeactivates: true, allowOutsideClick: true } Focus trap options
hideEmptyContent boolean false If true, hides the empty content when there are no options available.
id string - The unique identifier for the autocomplete component.
inputSize enum - Defines the input size of the autocomplete.
inputValue string -

The text value of the input.

Pass this together with onInputChange to control the input text externally, for example to implement server-side filtering.

intent enum - The intent of each item
isClearable boolean false Whether to include a clear button in the autocomplete component. If enabled, this allows users to clear the selection and input text. This option ignores the allowEmpty setting.
isDisabled boolean - Whether the autocomplete should be disabled, preventing user interaction.
isInvalid boolean false Marks the control invalid without supplying messages, for validation that is reported elsewhere.
isLoading boolean - If true, the autocomplete will show a loading spinner instead of the dropdown icon. The spinner is also shown automatically while an onSearch promise is pending.
isRequired boolean false Whether the field is required. Adds an asterisk to the label; it does not set the required attribute on the control itself.
items Array -
label string - The label text rendered above the control and associated with it via for. Use the :label block instead when the label needs markup.
middleware Array - Additional floating-ui middleware, for positioning behavior beyond what placement, offsetOptions, flipOptions, and shiftOptions cover.
name string - The name attribute for the autocomplete component, useful for form submissions.
offsetOptions enum 5
onAction function -
onBlur function - Callback fired when the autocomplete component loses focus.
onInputChange function - Callback fired whenever the user changes the input text.
onSearch function - Async search function. When provided, the component calls it (debounced) as the user types and renders the resolved items, showing a loading spinner while the returned promise is pending. Stale responses are ignored (latest query wins). Built-in filtering is disabled; @items is used as the initial list before the first search.
onSelectionChange function -

Callback fired when the selection changes.

Update your @selectedKey state in this callback to maintain two-way binding.

placeholder string - The placeholder text displayed when the input is empty.
placement enum 'bottom-start' Placement of the menu when open
popoverSize enum 'trigger'

Defines the size of the popover dropdown.

  • 'sm': Small
  • 'md': Medium
  • 'lg': Large
  • 'trigger': Same size as the trigger
renderInPlace boolean false Whether to render in place or in the specified/default destination
searchDebounce number 250 Debounce duration, in milliseconds, applied to onSearch calls.
searchMessage string - Message shown in the dropdown of an async autocomplete (onSearch) while the query is blank — e.g. "Type to search for an address…". Prompts the user to start typing when they open the dropdown by clicking. Also customizable via the searchMessage block.
selectedKey string -

The currently selected key.

Autocomplete is single-selection only — for a searchable multi-select, use Select with @isFilterable and @selectionMode="multiple".

Data Flow:

  • Pass this to set the initial selection
  • Update this in your onSelectionChange handler to maintain two-way binding
  • The component calls onSelectionChange whenever the user changes the selection
shiftOptions { padding?: Padding; mainAxis?: boolean; crossAxis?: boolean; rootBoundary?: RootBoundary; elementContext?: ElementContext; altBoundary?: boolean; limiter?: { ...; }; boundary?: Boundary; } - Options for the floating-ui shift middleware, which nudges the content along its axis to keep it in view.
startContentPointerEvents enum 'auto' Controls pointer-events property of startContent. If you want to pass the click event to the input, set it to none.
strategy enum 'absolute'
target enum -

The target where to render the portal. There are 3 options: 1) Element object, 2) element id, 3) portal target name.

For element id, string must be prefixed with #. If no value is passed in, we will render to the closest unnamed portal target, parent portal or document.body.

transition Object {name: 'overlay-transition--scale'} The transition to be used in the Modal.
transitionDuration number 200 Duration of the animation

Blocks

Name Type Default Description
item * Array -
default * Array -
startContent * Array - Content to display at the beginning of the autocomplete component. This can be an icon, a label, or any custom UI element.
endContent * Array - Content to display at the end of the autocomplete component. This can be an icon, a button, or any custom UI element.
emptyContent * Array - The content to display when there are no matching options. If hideEmptyContent argument is true, this content will not be shown.
searchMessage * Array - Message shown in the dropdown of an async autocomplete while the query is blank, prompting the user to start typing. Takes priority over the searchMessage argument.
Released under MIT License - Created by Josemar Luedke