---
title: Autocomplete
description: Text input with a suggestion list — value is the raw input string, not a discrete item selection
---

# 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`.

[Live preview in the UI Catalogue →](https://ui.tailor.tech/components/autocomplete)

## 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

| Prop              | Type                      | Default         | Description                                                         |
| ----------------- | ------------------------- | --------------- | ------------------------------------------------------------------- |
| `items`           | `I[]`                     | -               | Suggestion items. May be a flat array or an array of `ItemGroup<T>` |
| `placeholder`     | `string`                  | -               | Placeholder text for the input                                      |
| `emptyText`       | `string`                  | `"No results."` | Text shown when no items match                                      |
| `value`           | `string`                  | -               | Controlled value (raw input string)                                 |
| `defaultValue`    | `string`                  | -               | 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         |
| `className`       | `string`                  | -               | Additional CSS classes for the root container                       |
| `disabled`        | `boolean`                 | `false`         | Disables the autocomplete                                           |
| `aria-label`      | `string`                  | -               | Accessible name for the input. Use when there is no visible label   |
| `aria-labelledby` | `string`                  | -               | ID of the element(s) that label the input                           |
| `id`              | `string`                  | -               | ID applied to the combobox input element                            |

### 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:

| Prop           | Type                          | Default                    | Description                                                        |
| -------------- | ----------------------------- | -------------------------- | ------------------------------------------------------------------ |
| `fetcher`      | `AutocompleteAsyncFetcher<T>` | -                          | Fetcher called on each keystroke (debounced by default)            |
| `loadingText`  | `string`                      | `"Loading..."`             | Text shown while loading                                           |
| `errorText`    | `string`                      | `"Couldn't load results."` | Message shown in the popover when the fetcher fails (with Retry)   |
| `retryText`    | `string`                      | `"Retry"`                  | Label for the retry button in the error state                      |
| `onFetchError` | `(error: unknown) => void`    | -                          | Called once per outage when a fetch fails (logging/error tracking) |

### 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.

### Error handling

If the fetcher throws or rejects, `Autocomplete.Async` renders a built-in inline error state in the popover — the `errorText` message plus a **Retry** button that re-runs the last fetch — instead of the misleading "No results." empty state. Aborted/superseded requests are ignored, and announcements are de-duped per outage. Pass `onFetchError` to run a side effect (logging, toast) — it fires once per outage and re-arms after the next successful fetch.

## 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:

`Autocomplete.useAsync` accepts the same options as `Autocomplete.Async` (including `onFetchError`) and returns:

| Property  | Type         | Description                                             |
| --------- | ------------ | ------------------------------------------------------- |
| `items`   | `T[]`        | Currently loaded items                                  |
| `loading` | `boolean`    | Whether a fetch is in progress                          |
| `error`   | `unknown`    | The error thrown by the last fetch, if any              |
| `retry`   | `() => void` | Re-runs the last fetch (use to build a custom retry UI) |

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

<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.error ? (
          <button onClick={suggestions.retry}>Couldn't load. Retry</button>
        ) : 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
- When used standalone (no visible `<label>`), give the input an accessible name with `aria-label` or `aria-labelledby` — otherwise screen readers announce only the current value:

```tsx
<Autocomplete items={items} aria-label="City search" />
```

## Related Components

- [Select](./select.md) - Non-searchable discrete item selection
- [Combobox](./combobox.md) - Searchable combobox that returns discrete items
- [Input](./input.md) - Plain text input without suggestions
