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 { Autocomplete } from 'frontile';
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>
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>
}
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.
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>
}
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>
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>
}
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.
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>
}
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>
}
Autocomplete accepts the same form control arguments as other Frontile inputs: @label, @description, @errors, @isInvalid, @isRequired, and @inputSize.
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>
| 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 |
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.
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).
| 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 |
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 |
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.
|
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 Data Flow:
|
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) For element id, string must be prefixed with |
transition
|
Object
|
{name: 'overlay-transition--scale'}
|
The transition to be used in the Modal. |
transitionDuration
|
number
|
200
|
Duration of the animation |
| 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.
|