Autocomplete
PortedA text field that suggests values as you type. The value is the text: a suggestion fills it in, and any other text is a value too. Use it for searches and free answers, such as a name, a link or a company. When the value must be one of the options, use Combobox, which makes the user pick. Holds an AutocompleteInput and an AutocompleteContent; with label, description or error, it is wrapped in a Field.
import { Autocomplete, AutocompleteContent, AutocompleteInput, AutocompleteItem, AutocompleteList } from "@etched/monet/autocomplete";
const companies = ["Etched", "Contoso", "Fabrikam", "Northwind Traders", "Tailspin Toys", "Wide World Importers", "Woodgrove Bank"];
/** Company names, as the field's suggestions. */
function CompanySuggestions() {
return (
<AutocompleteContent>
<AutocompleteList>
{(company: string) => (
<AutocompleteItem key={company} value={company}>
{company}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
);
}
export function Preview() {
return (
<Autocomplete items={companies}>
<AutocompleteInput aria-label="Company" placeholder="Company" />
<CompanySuggestions />
</Autocomplete>
);
}Installation
import { Autocomplete, AutocompleteCollection, AutocompleteContent, AutocompleteEmpty, AutocompleteGroup, AutocompleteGroupLabel, AutocompleteInput, AutocompleteItem, AutocompleteList, AutocompleteSeparator, AutocompleteStatus } from "@etched/monet/autocomplete";Examples
Free text
The value is what is typed. A suggestion fills it in, and a name that is not in the list is a value too. When the value must be one of the options, use Combobox: it makes the user pick.
Choose a suggestion, or type any name.
import { useState } from "react";
import { Autocomplete, AutocompleteContent, AutocompleteInput, AutocompleteItem, AutocompleteList } from "@etched/monet/autocomplete";
const companies = ["Etched", "Contoso", "Fabrikam", "Northwind Traders", "Tailspin Toys", "Wide World Importers", "Woodgrove Bank"];
/** Company names, as the field's suggestions. */
function CompanySuggestions() {
return (
<AutocompleteContent>
<AutocompleteList>
{(company: string) => (
<AutocompleteItem key={company} value={company}>
{company}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
);
}
export function FreeText() {
const [company, setCompany] = useState("");
return (
<div className="grid">
<Autocomplete
label="Current company"
description={company ? `The value is “${company}”.` : "Choose a suggestion, or type any name."}
items={companies}
value={company}
onValueChange={setCompany}
>
<AutocompleteInput placeholder="Company" />
<CompanySuggestions />
</Autocomplete>
</div>
);
}Suggestions with details
Groups, with an icon, a description line and a count on each suggestion, as in Orbit's warm paths search. Enter on a highlighted suggestion fills the field; with none highlighted, Enter submits the form.
import { BuildingsIcon, GraduationCapIcon } from "@phosphor-icons/react";
import { useState } from "react";
import { Button } from "@etched/monet/button";
import { Autocomplete, AutocompleteCollection, AutocompleteContent, AutocompleteEmpty, AutocompleteGroup, AutocompleteGroupLabel, AutocompleteInput, AutocompleteItem, AutocompleteList } from "@etched/monet/autocomplete";
interface Organization {
value: string;
label: string;
description: string;
people: number;
}
interface OrganizationGroup {
label: string;
items: Organization[];
}
const organizations: OrganizationGroup[] = [
{
label: "Companies",
items: [
{ value: "contoso", label: "Contoso", description: "Company · Semiconductors", people: 14 },
{ value: "fabrikam", label: "Fabrikam", description: "Company · Hardware", people: 6 },
{ value: "northwind", label: "Northwind Traders", description: "Company · Logistics", people: 1 },
],
},
{
label: "Schools",
items: [
{ value: "north-state", label: "North State University", description: "School · Not matched on LinkedIn", people: 9 },
{ value: "west-tech", label: "West Tech", description: "School · West Institute of Technology", people: 3 },
],
},
];
export function SuggestionsWithDetails() {
const [query, setQuery] = useState("");
const [submitted, setSubmitted] = useState("");
return (
<form
className="row"
onSubmit={(event) => {
event.preventDefault();
setSubmitted(query);
}}
>
<Autocomplete items={organizations} value={query} onValueChange={setQuery}>
<AutocompleteInput aria-label="Company, school or LinkedIn link" placeholder="Company, school or LinkedIn link" style={{ inlineSize: "20rem" }} />
<AutocompleteContent>
<AutocompleteEmpty>No company or school matches.</AutocompleteEmpty>
<AutocompleteList>
{(group: OrganizationGroup) => (
<AutocompleteGroup key={group.label} items={group.items}>
<AutocompleteGroupLabel>{group.label}</AutocompleteGroupLabel>
<AutocompleteCollection>
{(organization: Organization) => (
<AutocompleteItem
key={organization.value}
value={organization}
icon={group.label === "Schools" ? <GraduationCapIcon /> : <BuildingsIcon />}
description={organization.description}
detail={`${organization.people} ${organization.people === 1 ? "person" : "people"}`}
>
{organization.label}
</AutocompleteItem>
)}
</AutocompleteCollection>
</AutocompleteGroup>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
<Button type="submit" variant="primary">
Find paths
</Button>
{submitted && <span>Searched for “{submitted}”</span>}
</form>
);
}Search on a server
filter={null} shows the suggestions as the app gets them. AutocompleteStatus says when it searches, and screen readers hear it.
import { useEffect, useState } from "react";
import { Autocomplete, AutocompleteContent, AutocompleteInput, AutocompleteItem, AutocompleteList, AutocompleteStatus } from "@etched/monet/autocomplete";
const companies = ["Etched", "Contoso", "Fabrikam", "Northwind Traders", "Tailspin Toys", "Wide World Importers", "Woodgrove Bank"];
export function SearchOnAServer() {
const [query, setQuery] = useState("");
const [found, setFound] = useState<string[]>([]);
const [searching, setSearching] = useState(false);
// A stand-in for a server search: suggestions come 300 ms after the last key.
useEffect(() => {
const words = query.trim().toLowerCase();
setSearching(Boolean(words));
setFound([]);
if (!words) return;
const timer = setTimeout(() => {
setFound(companies.filter((company) => company.toLowerCase().includes(words)));
setSearching(false);
}, 300);
return () => clearTimeout(timer);
}, [query]);
return (
<div className="grid">
<Autocomplete label="Company" items={found} filter={null} value={query} onValueChange={setQuery}>
<AutocompleteInput placeholder="Search companies" />
<AutocompleteContent>
<AutocompleteStatus>{searching ? "Searching…" : query.trim() && !found.length ? "No companies match." : null}</AutocompleteStatus>
<AutocompleteList>
{(company: string) => (
<AutocompleteItem key={company} value={company}>
{company}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
</Autocomplete>
</div>
);
}Sizes and states
import { Autocomplete, AutocompleteContent, AutocompleteInput, AutocompleteItem, AutocompleteList } from "@etched/monet/autocomplete";
const companies = ["Etched", "Contoso", "Fabrikam", "Northwind Traders", "Tailspin Toys", "Wide World Importers", "Woodgrove Bank"];
/** Company names, as the field's suggestions. */
function CompanySuggestions() {
return (
<AutocompleteContent>
<AutocompleteList>
{(company: string) => (
<AutocompleteItem key={company} value={company}>
{company}
</AutocompleteItem>
)}
</AutocompleteList>
</AutocompleteContent>
);
}
export function SizesAndStates() {
return (
<div className="grid">
<Autocomplete label="Small" items={companies}>
<AutocompleteInput size="sm" placeholder="Company" />
<CompanySuggestions />
</Autocomplete>
<Autocomplete label="Read-only" items={companies} defaultValue="Etched" readOnly>
<AutocompleteInput />
<CompanySuggestions />
</Autocomplete>
<Autocomplete label="Disabled" items={companies} defaultValue="Etched" disabled>
<AutocompleteInput />
<CompanySuggestions />
</Autocomplete>
<Autocomplete label="Previous company" items={companies} error="Enter the company they left.">
<AutocompleteInput placeholder="Company" />
<CompanySuggestions />
</Autocomplete>
</div>
);
}Accessibility
ArrowUp/ArrowDownopen the list and move the highlight.Enterfills in the highlighted suggestion; with none highlighted, it submits the form.Escapecloses the list and keeps the text.Escapeon a closed list clears the text.- The field is a
combobox, named bylabeloraria-label. Suggestions areoptions in alistbox; anAutocompleteGroupis agroupnamed by its label. - Focus stays in the field;
aria-activedescendantpoints to the highlight.aria-expandedis false while nothing shows.AutocompleteEmptyandAutocompleteStatusare politestatusregions. - Without
label, giveAutocompleteInputanaria-label. For a server search, add anAutocompleteStatusthat says "Searching…".
API reference
AutocompleteContent
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | – | |
| children * | ReactNode | – | AutocompleteList, and AutocompleteEmpty or AutocompleteStatus. |
| container | BaseAutocomplete.Portal.Props["container"] | document.body | The element the portal goes into, for example a shadow root. |
AutocompleteEmpty
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | – |
AutocompleteGroupLabel
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | – |
AutocompleteGroup
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | – |
AutocompleteInput
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | – | |
| size | "sm" | "base" | "base" | Same heights as Input. |
AutocompleteItem
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | – | |
| children * | ReactNode | – | The suggestion's text. |
| description | ReactNode | – | A second line, such as a kind or a place. |
| icon | ReactNode | – | Before the text, such as a logo or an icon. |
| detail | ReactNode | – | At the end of the row, such as a count. |
AutocompleteList
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | – |
Autocomplete
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | – | Visible label. Without one, give the control an aria-label. |
| labelTooltip | ReactNode | – | More about the field, behind an info button after the label. |
| labelTooltipLabel | string | "More information" | Names the info button. |
| description | ReactNode | – | Help text below the control. Hidden while an error shows. |
| error | FieldError | – | |
| items | readonly Item[] | readonly { items: readonly Item[] }[] | – | The suggestions: items, or groups that each have an items array (and anything else, such as a label). |
AutocompleteSeparator
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | – |
AutocompleteStatus
| Prop | Type | Default | Description |
|---|---|---|---|
| className | string | – |