Skip to content

Autocomplete

The Autocomplete component provides a free-text input with a suggestion list. Unlike Select and Combobox, the component value is the raw input string — not a discrete item selection. For async suggestions use Autocomplete.Async. For custom compositions use Autocomplete.Parts.

Import

tsx
import { Autocomplete } from "@tailor-platform/app-shell";

Basic Usage

tsx
<Autocomplete
  items={["Apple", "Banana", "Cherry"]}
  placeholder="Type a fruit..."
  onValueChange={(value) => console.log(value)}
/>

Props

Autocomplete Props

PropTypeDefaultDescription
itemsI[]-Suggestion items. May be a flat array or an array of ItemGroup<T>
placeholderstring-Placeholder text for the input
emptyTextstring"No results."Text shown when no items match
valuestring-Controlled value (raw input string)
defaultValuestring-Initial value (uncontrolled)
onValueChange(value: string) => void-Called when the value changes
mapItem(item: T) => MappedItem-Map each item to its label, key, and optional custom render
classNamestring-Additional CSS classes for the root container
disabledbooleanfalseDisables the autocomplete

MappedItem

ts
interface MappedItem {
  label: string; // Display text, used for filtering and a11y
  key?: string; // React key. Defaults to label
  render?: React.ReactNode; // Custom JSX to render in the dropdown
}

ItemGroup

ts
interface ItemGroup<T> {
  label: string;
  items: T[];
}

Grouped Suggestions

tsx
const cities = [
  { label: "Japan", items: ["Tokyo", "Osaka", "Kyoto"] },
  { label: "France", items: ["Paris", "Lyon", "Marseille"] },
];

<Autocomplete items={cities} placeholder="Search cities..." />;

Object Items with mapItem

When items are objects, use mapItem to tell the component how to display them. The selected value is the item's label string returned by mapItem:

tsx
type City = { code: string; name: string };

const cities: City[] = [
  { code: "TYO", name: "Tokyo" },
  { code: "OSA", name: "Osaka" },
];

<Autocomplete
  items={cities}
  mapItem={(city) => ({ label: city.name, key: city.code })}
  placeholder="Search cities..."
  onValueChange={(name) => console.log(name)} // receives "Tokyo", "Osaka", etc.
/>;

Async Suggestions

Use Autocomplete.Async to fetch suggestions as the user types. The fetcher is called on each keystroke (debounced). When the dropdown first opens or the input is cleared, the fetcher receives null as the query — return initial/default suggestions for null, or return an empty array to show nothing until the user starts typing.

tsx
import { type AutocompleteAsyncFetcher } from "@tailor-platform/app-shell";

const fetcher: AutocompleteAsyncFetcher<string> = async (query, { signal }) => {
  const res = await fetch(`/api/suggestions?q=${query ?? ""}`, { signal });
  return res.json();
};

<Autocomplete.Async
  fetcher={fetcher}
  placeholder="Search..."
  onValueChange={(value) => console.log(value)}
/>;

Autocomplete.Async Props

Accepts all the same props as Autocomplete except items, plus:

PropTypeDefaultDescription
fetcherAutocompleteAsyncFetcher<T>-Fetcher called on each keystroke (debounced by default)
loadingTextstring"Loading..."Text shown while loading

AutocompleteAsyncFetcher

ts
type AutocompleteAsyncFetcher<T> =
  | ((query: string | null, options: { signal: AbortSignal }) => Promise<T[]>)
  | {
      fn: (query: string | null, options: { signal: AbortSignal }) => Promise<T[]>;
      debounceMs: number;
    };

query is null when the user has not typed anything (e.g. the dropdown was just opened or the input was cleared). Pass { fn, debounceMs } to customize the debounce delay. Errors thrown by the fetcher are silently caught — handle errors inside the fetcher.

Low-level Primitives

Autocomplete.Parts exposes styled sub-components and hooks for fully custom compositions:

tsx
const {
  Root,
  Value,
  InputGroup,
  Input,
  Trigger,
  Content,
  List,
  Item,
  Empty,
  Clear,
  Group,
  GroupLabel,
  Collection,
  Status,
} = Autocomplete.Parts;

Examples

Controlled Autocomplete

tsx
const [query, setQuery] = useState("");

<Autocomplete items={suggestions} value={query} onValueChange={setQuery} placeholder="Search..." />;

Address Input with Async Suggestions

tsx
const fetcher: AutocompleteAsyncFetcher<string> = async (query, { signal }) => {
  if (!query) return [];
  const res = await fetch(`/api/address-lookup?q=${encodeURIComponent(query)}`, { signal });
  return res.json();
};

<Autocomplete.Async
  fetcher={fetcher}
  placeholder="Start typing an address..."
  onValueChange={(address) => form.setValue("address", address)}
/>;

Async with Parts (custom composition)

Combine Autocomplete.useAsync with Autocomplete.Parts for full control over layout and rendering:

tsx
const suggestions = Autocomplete.useAsync({
  fetcher: async (query, { signal }) => {
    const res = await fetch(`/api/suggestions?q=${query ?? ""}`, { signal });
    return res.json();
  },
});

<Autocomplete.Parts.Root {...suggestions} filter={null}>
  <Autocomplete.Parts.InputGroup>
    <Autocomplete.Parts.Input placeholder="Search..." />
    <Autocomplete.Parts.Clear />
  </Autocomplete.Parts.InputGroup>
  <Autocomplete.Parts.Content>
    <Autocomplete.Parts.List>
      {suggestions.items.map((item) => (
        <Autocomplete.Parts.Item key={item} value={item}>
          {item}
        </Autocomplete.Parts.Item>
      ))}
      <Autocomplete.Parts.Empty>
        {suggestions.loading ? "Loading..." : "No results."}
      </Autocomplete.Parts.Empty>
    </Autocomplete.Parts.List>
  </Autocomplete.Parts.Content>
</Autocomplete.Parts.Root>;

Accessibility

  • Input is keyboard accessible with arrow key navigation
  • Pressing Escape closes the suggestion list
  • Selecting a suggestion fills the input with the item's label
  • Select - Non-searchable discrete item selection
  • Combobox - Searchable combobox that returns discrete items
  • Input - Plain text input without suggestions