# Autocomplete (/docs/autocomplete)





## API Reference [#api-reference]

<BaseUIReference href="https://base-ui.com/react/components/autocomplete" />

## Choosing the right component [#choosing-the-right-component]

Use `Autocomplete` when users need to type freely and you want to suggest matching options while
they type.

* Choose &#x2A;*`Autocomplete`** for free-form values with suggestions.
* Choose &#x2A;*`Combobox`** when users should select from predefined options, with typing used to filter.
* Choose &#x2A;*`Select`** for strict pickers with no text input in the trigger.

## Basic [#basic]

<Preview cssProperties="autocompletePlaygroundCssProperties">
  <AutocompleteExample />

  <Preview.Code>
    {`
          import {
            Autocomplete,
            AutocompleteField,
            AutocompleteFieldLabel,
            AutocompleteInputGroup,
            AutocompleteInput,
            AutocompleteControlActions,
            AutocompleteClear,
            AutocompleteTrigger,
            AutocompleteContent,
            AutocompleteEmpty,
            AutocompleteList,
            AutocompleteItem,
            AutocompleteItemText,
          } from "moduix";
          import { useId } from "react";

          export function AutocompleteDemo() {
            const id = useId();

            return (
              <Autocomplete items={tags} itemToStringValue={(item) => item.value}>
                <AutocompleteField>
                  <AutocompleteFieldLabel htmlFor={id}>Search tags</AutocompleteFieldLabel>
                  <AutocompleteInputGroup>
                    <AutocompleteInput id={id} placeholder="e.g. feature" />
                    <AutocompleteControlActions>
                      <AutocompleteClear aria-label="Clear value" />
                      <AutocompleteTrigger aria-label="Open suggestions" />
                    </AutocompleteControlActions>
                  </AutocompleteInputGroup>
                </AutocompleteField>

                <AutocompleteContent>
                  <AutocompleteEmpty>No tags found.</AutocompleteEmpty>
                  <AutocompleteList>
                    {(item) => (
                      <AutocompleteItem key={item.id} value={item}>
                        <AutocompleteItemText>{item.value}</AutocompleteItemText>
                      </AutocompleteItem>
                    )}
                  </AutocompleteList>
                </AutocompleteContent>
              </Autocomplete>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const tags = [
            { id: "t1", value: "feature" },
            { id: "t2", value: "fix" },
            { id: "t3", value: "bug" },
            { id: "t4", value: "docs" },
          ];
        `}
  </Preview.Data>

  <Preview.CSSProperties>
    {(context) => <AutocompleteCssPropertiesPanel {...context} />}
  </Preview.CSSProperties>

  <Preview.CSSPlayground>
    {(context) => <AutocompleteCssPlaygroundPanel {...context} />}
  </Preview.CSSPlayground>
</Preview>

## Anatomy [#anatomy]

`Autocomplete` combines an input field, control actions, and a popup with filtered options. Keep
input-related parts together inside `AutocompleteField` so labeling and keyboard behavior stay
connected.

```text
Autocomplete
├─ AutocompleteField
│  ├─ AutocompleteFieldLabel
│  └─ AutocompleteInputGroup
│     ├─ AutocompleteInput
│     └─ AutocompleteControlActions (optional)
│        ├─ AutocompleteClear (optional)
│        └─ AutocompleteTrigger (optional)
└─ AutocompleteContent
   ├─ AutocompleteStatus (optional)
   ├─ AutocompleteEmpty
   ├─ AutocompleteSeparator (optional)
   └─ AutocompleteList
      ├─ AutocompleteItem
      │  └─ AutocompleteItemText
      ├─ AutocompleteRow (optional, for grid lists)
      │  └─ AutocompleteItem
      └─ AutocompleteGroup (optional)
         ├─ AutocompleteGroupLabel
         └─ AutocompleteCollection
            └─ AutocompleteItem
```

```tsx
<Autocomplete items={tags} itemToStringValue={(item) => item.value}>
  <AutocompleteField>
    <AutocompleteFieldLabel htmlFor={id}>Search tags</AutocompleteFieldLabel>
    <AutocompleteInputGroup>
      <AutocompleteInput id={id} placeholder="e.g. feature" />
      <AutocompleteControlActions>
        <AutocompleteClear aria-label="Clear value" />
        <AutocompleteTrigger aria-label="Open suggestions" />
      </AutocompleteControlActions>
    </AutocompleteInputGroup>
  </AutocompleteField>

  <AutocompleteContent>
    <AutocompleteEmpty>No tags found.</AutocompleteEmpty>
    <AutocompleteList>
      {(item) => (
        <AutocompleteItem key={item.id} value={item}>
          <AutocompleteItemText>{item.value}</AutocompleteItemText>
        </AutocompleteItem>
      )}
    </AutocompleteList>
  </AutocompleteContent>
</Autocomplete>
```

| Part                         | Role                                                                                               |
| ---------------------------- | -------------------------------------------------------------------------------------------------- |
| `Autocomplete`               | Root state machine. Handles value, filtering, highlighting, and open/close behavior.               |
| `AutocompleteField`          | Semantic wrapper for field-level structure. Keeps label and input wiring in one place.             |
| `AutocompleteFieldLabel`     | Accessible label for the input or field trigger.                                                   |
| `AutocompleteInputGroup`     | Layout wrapper for the input and optional control actions.                                         |
| `AutocompleteInput`          | Text input where users type a query or free-form value.                                            |
| `AutocompleteControlActions` | Optional container for clear/open controls.                                                        |
| `AutocompleteClear`          | Optional action that clears the current input value.                                               |
| `AutocompleteTrigger`        | Optional action that toggles or opens the suggestion popup.                                        |
| `AutocompleteContent`        | Popup surface. Also renders service slots (`portal`, `backdrop`, `positioner`, `arrow`) via props. |
| `AutocompleteStatus`         | Optional status region for loading or result-count feedback.                                       |
| `AutocompleteEmpty`          | Empty-state content shown when no items match.                                                     |
| `AutocompleteList`           | Collection container for options.                                                                  |
| `AutocompleteRow`            | Optional row wrapper when `grid` is enabled on the root.                                           |
| `AutocompleteItem`           | One selectable option in the list.                                                                 |
| `AutocompleteSeparator`      | Optional visual separator between groups or custom sections.                                       |
| `AutocompleteGroup`          | Optional grouped section with its own item subset.                                                 |
| `AutocompleteCollection`     | Renderer for group items inside `AutocompleteGroup`.                                               |

Use default styling for service slots in most cases. Customize `portal`, `backdrop`,
`positioner`, and `arrow` when you need custom layering, overlay behavior, popup placement, or
arrow visuals.

## Composition [#composition]

Use `Autocomplete` for root state and Base UI behavior props such as `items`, `value`,
`onValueChange`, `filter`, `limit`, `mode`, `autoHighlight`, and `openOnInputClick`.

The root keeps the Base UI behavior surface intact. Use `itemToStringValue` for object items,
`filter={null}` or `filteredItems` for external filtering, `grid` with `AutocompleteRow` for grid
navigation, `modal` for modal popup behavior, `submitOnItemClick` for single-field search forms,
`onOpenChange` for controlled popup side effects, `virtualized` for externally virtualized lists,
and `keepHighlight` or `highlightItemOnHover` when pointer and keyboard highlighting need custom
rules. `actionsRef` is available for manual unmount flows with animation libraries.
`useAutocompleteFilter` exposes Base UI string matching helpers, and
`useAutocompleteFilteredItems` returns the currently filtered items for custom list layouts.

`className` styles the visible popup. `classNames` styles service slots hidden from the default
composition: `portal`, `backdrop`, `positioner`, and `arrow`. Use `container`, `portalProps`,
`backdropProps`, `positionerProps`, `arrowProps`, `withBackdrop`, and `arrow` when you need the
matching Base UI escape hatches:

```tsx
<AutocompleteContent
  className={styles.popup}
  sideOffset={8}
  withArrow
  withBackdrop
  classNames={{
    portal: styles.portal,
    backdrop: styles.backdrop,
    positioner: styles.positioner,
    arrow: styles.arrow,
  }}
/>
```

## Examples [#examples]

### Grouped [#grouped]

Pass grouped data and render `AutocompleteCollection` inside each `AutocompleteGroup`.

<Preview>
  <GroupedAutocompleteExample />

  <Preview.Code>
    {`
          import {
            Autocomplete,
            AutocompleteField,
            AutocompleteFieldLabel,
            AutocompleteInputGroup,
            AutocompleteInput,
            AutocompleteControlActions,
            AutocompleteClear,
            AutocompleteTrigger,
            AutocompleteContent,
            AutocompleteEmpty,
            AutocompleteList,
            AutocompleteGroup,
            AutocompleteGroupLabel,
            AutocompleteCollection,
            AutocompleteItem,
            AutocompleteItemText,
          } from "moduix";
          import { useId } from "react";

          export function GroupedAutocompleteDemo() {
            const id = useId();

            return (
              <Autocomplete items={groupedTags} itemToStringValue={(item) => item.value}>
                <AutocompleteField>
                  <AutocompleteFieldLabel htmlFor={id}>Search grouped tags</AutocompleteFieldLabel>
                  <AutocompleteInputGroup>
                    <AutocompleteInput id={id} placeholder="e.g. docs" />
                    <AutocompleteControlActions>
                      <AutocompleteClear aria-label="Clear value" />
                      <AutocompleteTrigger aria-label="Open suggestions" />
                    </AutocompleteControlActions>
                  </AutocompleteInputGroup>
                </AutocompleteField>

                <AutocompleteContent>
                  <AutocompleteEmpty>No tags found.</AutocompleteEmpty>
                  <AutocompleteList>
                    {(group) => (
                      <AutocompleteGroup key={group.value} items={group.items}>
                        <AutocompleteGroupLabel>{group.value}</AutocompleteGroupLabel>
                        <AutocompleteCollection>
                          {(item) => (
                            <AutocompleteItem key={item.id} value={item}>
                              <AutocompleteItemText>{item.value}</AutocompleteItemText>
                            </AutocompleteItem>
                          )}
                        </AutocompleteCollection>
                      </AutocompleteGroup>
                    )}
                  </AutocompleteList>
                </AutocompleteContent>
              </Autocomplete>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const groupedTags = [
            {
              value: "General",
              items: [
                { id: "gt-1", value: "feature" },
                { id: "gt-2", value: "fix" },
                { id: "gt-3", value: "docs" },
              ],
            },
            {
              value: "Scope",
              items: [
                { id: "gt-4", value: "internal" },
                { id: "gt-5", value: "mobile" },
                { id: "gt-6", value: "backend" },
              ],
            },
          ];
        `}
  </Preview.Data>
</Preview>

### Item Icons [#item-icons]

Use item text slots for richer option content. Control icons can be replaced by passing children to `AutocompleteTrigger`, `AutocompleteClear`, or `AutocompleteIcon`.

<Preview>
  <ItemIconsAutocompleteExample />

  <Preview.Code>
    {`
          import {
            Autocomplete,
            AutocompleteField,
            AutocompleteFieldLabel,
            AutocompleteInputGroup,
            AutocompleteInput,
            AutocompleteControlActions,
            AutocompleteClear,
            AutocompleteTrigger,
            AutocompleteContent,
            AutocompleteEmpty,
            AutocompleteList,
            AutocompleteItemText,
            AutocompleteItemTextContent,
            AutocompleteItemTextIcon,
            ChevronUpIcon,
            CloseLineIcon,
            InfoIcon,
            AutocompleteItemTextLabel,
            AutocompleteItem,
          } from "moduix";
          import { useId } from "react";

          export function ItemIconsAutocompleteDemo() {
            const id = useId();

            return (
              <Autocomplete items={tags} itemToStringValue={(item) => item.value}>
                <AutocompleteField>
                  <AutocompleteFieldLabel htmlFor={id}>Search tags with icons</AutocompleteFieldLabel>
                  <AutocompleteInputGroup>
                    <AutocompleteInput id={id} placeholder="e.g. feature" />
                    <AutocompleteControlActions>
                      <AutocompleteClear aria-label="Clear value">
                        <CloseLineIcon />
                      </AutocompleteClear>
                      <AutocompleteTrigger aria-label="Open suggestions">
                        <ChevronUpIcon />
                      </AutocompleteTrigger>
                    </AutocompleteControlActions>
                  </AutocompleteInputGroup>
                </AutocompleteField>

                <AutocompleteContent>
                  <AutocompleteEmpty>No tags found.</AutocompleteEmpty>
                  <AutocompleteList>
                    {(item) => (
                      <AutocompleteItem key={item.id} value={item}>
                        <AutocompleteItemText>
                          <AutocompleteItemTextContent>
                            <AutocompleteItemTextIcon>
                              <InfoIcon />
                            </AutocompleteItemTextIcon>
                            <AutocompleteItemTextLabel>{item.value}</AutocompleteItemTextLabel>
                          </AutocompleteItemTextContent>
                        </AutocompleteItemText>
                      </AutocompleteItem>
                    )}
                  </AutocompleteList>
                </AutocompleteContent>
              </Autocomplete>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const tags = [
            { id: "t1", value: "feature" },
            { id: "t2", value: "fix" },
            { id: "t3", value: "bug" },
            { id: "t4", value: "docs" },
          ];
        `}
  </Preview.Data>
</Preview>

### Input Inside Popup [#input-inside-popup]

Use `AutocompleteFieldTrigger` when the visible field opens a popup that contains the searchable input.

<Preview>
  <InputInsidePopupAutocompleteExample />

  <Preview.Code>
    {`
          import {
            Autocomplete,
            AutocompleteField,
            AutocompleteFieldLabel,
            AutocompleteFieldTrigger,
            AutocompleteValue,
            AutocompleteIcon,
            AutocompleteContent,
            AutocompleteInlineInputContainer,
            AutocompleteInput,
            AutocompleteEmpty,
            AutocompleteList,
            AutocompleteItem,
            AutocompleteItemText,
          } from "moduix";

          export function InputInsidePopupAutocompleteDemo() {
            return (
              <Autocomplete items={tags} itemToStringValue={(item) => item.value}>
                <AutocompleteField>
                  <AutocompleteFieldLabel>Tag</AutocompleteFieldLabel>
                  <AutocompleteFieldTrigger>
                    <AutocompleteValue>{(value) => value || "Type to search"}</AutocompleteValue>
                    <AutocompleteIcon />
                  </AutocompleteFieldTrigger>
                </AutocompleteField>

                <AutocompleteContent>
                  <AutocompleteInlineInputContainer>
                    <AutocompleteInput placeholder="Search tag" />
                  </AutocompleteInlineInputContainer>
                  <AutocompleteEmpty>No tags found.</AutocompleteEmpty>
                  <AutocompleteList>
                    {(item) => (
                      <AutocompleteItem key={item.id} value={item}>
                        <AutocompleteItemText>{item.value}</AutocompleteItemText>
                      </AutocompleteItem>
                    )}
                  </AutocompleteList>
                </AutocompleteContent>
              </Autocomplete>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const tags = [
            { id: "t1", value: "feature" },
            { id: "t2", value: "fix" },
            { id: "t3", value: "bug" },
            { id: "t4", value: "docs" },
          ];
        `}
  </Preview.Data>
</Preview>

### Limit [#limit]

Set `limit` to cap the number of rendered matches.

<Preview>
  <LimitAutocompleteExample />

  <Preview.Code>
    {`
          import {
            Autocomplete,
            AutocompleteField,
            AutocompleteFieldLabel,
            AutocompleteInputGroup,
            AutocompleteInput,
            AutocompleteControlActions,
            AutocompleteClear,
            AutocompleteTrigger,
            AutocompleteContent,
            AutocompleteEmpty,
            AutocompleteList,
            AutocompleteItem,
            AutocompleteItemText,
          } from "moduix";
          import { useId } from "react";

          export function LimitAutocompleteDemo() {
            const id = useId();

            return (
              <Autocomplete
                items={topMovies}
                itemToStringValue={(item) => item.title}
                limit={5}
                openOnInputClick
              >
                <AutocompleteField>
                  <AutocompleteFieldLabel htmlFor={id}>Top 5 matches</AutocompleteFieldLabel>
                  <AutocompleteInputGroup>
                    <AutocompleteInput id={id} placeholder="Type movie title" />
                    <AutocompleteControlActions>
                      <AutocompleteClear aria-label="Clear value" />
                      <AutocompleteTrigger aria-label="Open suggestions" />
                    </AutocompleteControlActions>
                  </AutocompleteInputGroup>
                </AutocompleteField>

                <AutocompleteContent>
                  <AutocompleteEmpty>No movies found.</AutocompleteEmpty>
                  <AutocompleteList>
                    {(movie) => (
                      <AutocompleteItem key={movie.id} value={movie}>
                        <AutocompleteItemText>
                          {movie.title} ({movie.year})
                        </AutocompleteItemText>
                      </AutocompleteItem>
                    )}
                  </AutocompleteList>
                </AutocompleteContent>
              </Autocomplete>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const topMovies = [
            { id: "1", title: "The Shawshank Redemption", year: 1994 },
            { id: "2", title: "The Godfather", year: 1972 },
            { id: "3", title: "The Dark Knight", year: 2008 },
            { id: "4", title: "Pulp Fiction", year: 1994 },
            { id: "5", title: "Forrest Gump", year: 1994 },
            { id: "6", title: "Inception", year: 2010 },
          ];
        `}
  </Preview.Data>
</Preview>

### Auto Highlight [#auto-highlight]

Use `autoHighlight="always"` when the first match should be active immediately.

<Preview>
  <AutoHighlightAutocompleteExample />

  <Preview.Code>
    {`
          import {
            Autocomplete,
            AutocompleteField,
            AutocompleteFieldLabel,
            AutocompleteInputGroup,
            AutocompleteInput,
            AutocompleteControlActions,
            AutocompleteClear,
            AutocompleteTrigger,
            AutocompleteContent,
            AutocompleteEmpty,
            AutocompleteList,
            AutocompleteItem,
            AutocompleteItemText,
          } from "moduix";
          import { useId } from "react";

          export function AutoHighlightAutocompleteDemo() {
            const id = useId();

            return (
              <Autocomplete
                items={tags}
                itemToStringValue={(item) => item.value}
                autoHighlight="always"
                mode="list"
              >
                <AutocompleteField>
                  <AutocompleteFieldLabel htmlFor={id}>Auto highlight</AutocompleteFieldLabel>
                  <AutocompleteInputGroup>
                    <AutocompleteInput id={id} placeholder="Use arrow keys or type" />
                    <AutocompleteControlActions>
                      <AutocompleteClear aria-label="Clear value" />
                      <AutocompleteTrigger aria-label="Open suggestions" />
                    </AutocompleteControlActions>
                  </AutocompleteInputGroup>
                </AutocompleteField>

                <AutocompleteContent>
                  <AutocompleteEmpty>No tags found.</AutocompleteEmpty>
                  <AutocompleteList>
                    {(item) => (
                      <AutocompleteItem key={item.id} value={item}>
                        <AutocompleteItemText>{item.value}</AutocompleteItemText>
                      </AutocompleteItem>
                    )}
                  </AutocompleteList>
                </AutocompleteContent>
              </Autocomplete>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const tags = [
            { id: "t1", value: "feature" },
            { id: "t2", value: "fix" },
            { id: "t3", value: "bug" },
            { id: "t4", value: "docs" },
          ];
        `}
  </Preview.Data>
</Preview>

### Grid [#grid]

Enable `grid` and render rows manually when options should navigate as a grid.

<Preview>
  <GridAutocompleteExample />

  <Preview.Code>
    {`
          import {
            Autocomplete,
            AutocompleteField,
            AutocompleteFieldLabel,
            AutocompleteInputGroup,
            AutocompleteInput,
            AutocompleteControlActions,
            AutocompleteClear,
            AutocompleteTrigger,
            AutocompleteContent,
            AutocompleteEmpty,
            AutocompleteList,
            AutocompleteRow,
            AutocompleteItem,
            AutocompleteItemText,
            useAutocompleteFilteredItems,
          } from "moduix";
          import { useId } from "react";

          function ShortcutGrid() {
            const filteredItems = useAutocompleteFilteredItems();

            if (filteredItems.length === 0) {
              return null;
            }

            return (
              <AutocompleteList>
                {chunkArray(filteredItems, 6).map((row) => (
                  <AutocompleteRow key={row.map((item) => item.id).join("-")}>
                    {row.map((item) => (
                      <AutocompleteItem key={item.id} value={item} aria-label={item.label}>
                        <AutocompleteItemText>{item.value}</AutocompleteItemText>
                      </AutocompleteItem>
                    ))}
                  </AutocompleteRow>
                ))}
              </AutocompleteList>
            );
          }

          export function GridAutocompleteDemo() {
            const id = useId();

            return (
              <Autocomplete
                items={shortcuts}
                itemToStringValue={(item) => item.label}
                grid
                openOnInputClick
              >
                <AutocompleteField>
                  <AutocompleteFieldLabel htmlFor={id}>Shortcut command</AutocompleteFieldLabel>
                  <AutocompleteInputGroup>
                    <AutocompleteInput id={id} placeholder="Type a command" />
                    <AutocompleteControlActions>
                      <AutocompleteClear aria-label="Clear value" />
                      <AutocompleteTrigger aria-label="Open suggestions" />
                    </AutocompleteControlActions>
                  </AutocompleteInputGroup>
                </AutocompleteField>

                <AutocompleteContent>
                  <AutocompleteEmpty>No shortcuts found.</AutocompleteEmpty>
                  <ShortcutGrid />
                </AutocompleteContent>
              </Autocomplete>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const shortcuts = [
            { id: "s1", value: "N", label: "New file" },
            { id: "s2", value: "O", label: "Open file" },
            { id: "s3", value: "S", label: "Save file" },
            { id: "s4", value: "P", label: "Print" },
            { id: "s5", value: "F", label: "Find" },
            { id: "s6", value: "R", label: "Replace" },
          ];

          function chunkArray(items, size) {
            const chunks = [];

            for (let index = 0; index < items.length; index += size) {
              chunks.push(items.slice(index, index + size));
            }

            return chunks;
          }
        `}
  </Preview.Data>
</Preview>

### Fuzzy [#fuzzy]

Provide a custom `filter` when matching should differ from the default string comparison.

<Preview>
  <FuzzyAutocompleteExample />

  <Preview.Code>
    {`
          import {
            Autocomplete,
            AutocompleteField,
            AutocompleteFieldLabel,
            AutocompleteInputGroup,
            AutocompleteInput,
            AutocompleteControlActions,
            AutocompleteClear,
            AutocompleteTrigger,
            AutocompleteContent,
            AutocompleteEmpty,
            AutocompleteList,
            AutocompleteItem,
            AutocompleteItemText,
          } from "moduix";
          import { useId } from "react";

          export function FuzzyAutocompleteDemo() {
            const id = useId();

            return (
              <Autocomplete
                items={topMovies}
                itemToStringValue={(item) => item.title}
                filter={(item, query, itemToString) => {
                  const label = itemToString ? itemToString(item) : String(item);

                  return isFuzzyMatch(label, query);
                }}
              >
                <AutocompleteField>
                  <AutocompleteFieldLabel htmlFor={id}>Fuzzy search</AutocompleteFieldLabel>
                  <AutocompleteInputGroup>
                    <AutocompleteInput id={id} placeholder="e.g. tdk or sra" />
                    <AutocompleteControlActions>
                      <AutocompleteClear aria-label="Clear value" />
                      <AutocompleteTrigger aria-label="Open suggestions" />
                    </AutocompleteControlActions>
                  </AutocompleteInputGroup>
                </AutocompleteField>

                <AutocompleteContent>
                  <AutocompleteEmpty>No movies found.</AutocompleteEmpty>
                  <AutocompleteList>
                    {(movie) => (
                      <AutocompleteItem key={movie.id} value={movie}>
                        <AutocompleteItemText>
                          {movie.title} ({movie.year})
                        </AutocompleteItemText>
                      </AutocompleteItem>
                    )}
                  </AutocompleteList>
                </AutocompleteContent>
              </Autocomplete>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const topMovies = [
            { id: "1", title: "The Shawshank Redemption", year: 1994 },
            { id: "2", title: "The Godfather", year: 1972 },
            { id: "3", title: "The Dark Knight", year: 2008 },
            { id: "4", title: "Pulp Fiction", year: 1994 },
          ];

          function isFuzzyMatch(value, query) {
            const normalizedValue = value.toLowerCase().trim();
            const normalizedQuery = query.toLowerCase().trim();

            if (normalizedQuery === "") {
              return true;
            }

            let queryIndex = 0;

            for (const character of normalizedValue) {
              if (character === normalizedQuery[queryIndex]) {
                queryIndex += 1;

                if (queryIndex === normalizedQuery.length) {
                  return true;
                }
              }
            }

            return false;
          }
        `}
  </Preview.Data>
</Preview>

### Async Search [#async-search]

Control `value`, disable the built-in filter with `filter={null}`, and keep `AutocompleteStatus` mounted while its children change.

<Preview>
  <AsyncSearchAutocompleteExample />

  <Preview.Code>
    {`
          import {
            Autocomplete,
            AutocompleteField,
            AutocompleteFieldLabel,
            AutocompleteInputGroup,
            AutocompleteInput,
            AutocompleteControlActions,
            AutocompleteClear,
            AutocompleteTrigger,
            AutocompleteContent,
            AutocompleteStatus,
            AutocompleteEmpty,
            AutocompleteList,
            AutocompleteItem,
            AutocompleteItemText,
            useAutocompleteFilter,
          } from "moduix";
          import { useId, useRef, useState, useTransition } from "react";

          export function AsyncSearchAutocompleteDemo() {
            const id = useId();
            const { contains } = useAutocompleteFilter();
            const [value, setValue] = useState("");
            const [searchResults, setSearchResults] = useState([]);
            const [isPending, startTransition] = useTransition();
            const abortControllerRef = useRef(null);
            const trimmedValue = value.trim();

            const status = isPending
              ? "Searching..."
              : trimmedValue !== "" && searchResults.length === 0
                ? \`No matches for "\${trimmedValue}".\`
                : null;

            return (
              <Autocomplete
                items={searchResults}
                value={value}
                filter={null}
                itemToStringValue={(item) => item.title}
                onValueChange={(nextValue) => {
                  setValue(nextValue);

                  const controller = new AbortController();
                  abortControllerRef.current?.abort();
                  abortControllerRef.current = controller;

                  if (nextValue.trim() === "") {
                    setSearchResults([]);
                    return;
                  }

                  startTransition(async () => {
                    await new Promise((resolve) => setTimeout(resolve, 250));

                    if (controller.signal.aborted) {
                      return;
                    }

                    setSearchResults(
                      topMovies.filter(
                        (movie) =>
                          contains(movie.title, nextValue) ||
                          contains(movie.year.toString(), nextValue),
                      ),
                    );
                  });
                }}
              >
                <AutocompleteField>
                  <AutocompleteFieldLabel htmlFor={id}>
                    Search movies by name or year
                  </AutocompleteFieldLabel>
                  <AutocompleteInputGroup>
                    <AutocompleteInput id={id} placeholder="e.g. Pulp Fiction or 1994" />
                    <AutocompleteControlActions>
                      <AutocompleteClear aria-label="Clear value" />
                      <AutocompleteTrigger aria-label="Open suggestions" />
                    </AutocompleteControlActions>
                  </AutocompleteInputGroup>
                </AutocompleteField>

                <AutocompleteContent>
                  <AutocompleteStatus>{status}</AutocompleteStatus>
                  <AutocompleteEmpty>
                    {trimmedValue !== "" && !isPending ? "Try a different query." : null}
                  </AutocompleteEmpty>
                  <AutocompleteList>
                    {(movie) => (
                      <AutocompleteItem key={movie.id} value={movie}>
                        <AutocompleteItemText>
                          {movie.title} ({movie.year})
                        </AutocompleteItemText>
                      </AutocompleteItem>
                    )}
                  </AutocompleteList>
                </AutocompleteContent>
              </Autocomplete>
            );
          }
        `}
  </Preview.Code>

  <Preview.Data>
    {`
          const topMovies = [
            { id: "1", title: "The Shawshank Redemption", year: 1994 },
            { id: "2", title: "The Godfather", year: 1972 },
            { id: "3", title: "The Dark Knight", year: 2008 },
            { id: "4", title: "Pulp Fiction", year: 1994 },
          ];
        `}
  </Preview.Data>
</Preview>

### Custom Styles [#custom-styles]

Style the popup directly with `className` and service slots with `classNames`.

<Preview>
  <CustomStylesAutocompleteExample />

  <Preview.Code>
    {`
          import {
            Autocomplete,
            AutocompleteField,
            AutocompleteFieldLabel,
            AutocompleteInputGroup,
            AutocompleteInput,
            AutocompleteControlActions,
            AutocompleteClear,
            AutocompleteTrigger,
            AutocompleteContent,
            AutocompleteEmpty,
            AutocompleteList,
            AutocompleteItem,
            AutocompleteItemText,
          } from "moduix";
          import { useId } from "react";

          export function CustomStylesAutocompleteDemo() {
            const id = useId();

            return (
              <Autocomplete items={tags} itemToStringValue={(item) => item.value}>
                <AutocompleteField>
                  <AutocompleteFieldLabel htmlFor={id}>Search tags</AutocompleteFieldLabel>
                  <AutocompleteInputGroup>
                    <AutocompleteInput id={id} placeholder="e.g. feature" />
                    <AutocompleteControlActions>
                      <AutocompleteClear aria-label="Clear value" />
                      <AutocompleteTrigger aria-label="Open suggestions" />
                    </AutocompleteControlActions>
                  </AutocompleteInputGroup>
                </AutocompleteField>

                <AutocompleteContent
                  className={styles.customPopup}
                  sideOffset={8}
                  withArrow
                  withBackdrop
                  classNames={{
                    portal: styles.customPortal,
                    backdrop: styles.customBackdrop,
                    positioner: styles.customPositioner,
                    arrow: styles.customArrow,
                  }}
                >
                  <AutocompleteEmpty>No tags found.</AutocompleteEmpty>
                  <AutocompleteList>
                    {(item) => (
                      <AutocompleteItem key={item.id} value={item}>
                        <AutocompleteItemText>{item.value}</AutocompleteItemText>
                      </AutocompleteItem>
                    )}
                  </AutocompleteList>
                </AutocompleteContent>
              </Autocomplete>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .customPortal {
            position: relative;
          }

          .customPositioner {
            --autocomplete-popup-max-height: 18rem;
          }

          .customPopup {
            --autocomplete-popup-bg: color-mix(in srgb, var(--color-popover) 92%, var(--color-primary));
            --autocomplete-popup-border-color: color-mix(in srgb, var(--color-border) 72%, var(--color-primary));
            --autocomplete-shadow: var(--shadow-md);
          }

          .customBackdrop {
            --autocomplete-backdrop-bg: var(--color-overlay);
            --autocomplete-backdrop-blur: 2px;
          }

          .customArrow {
            --autocomplete-arrow-stroke-color: var(--color-border);
          }
        `}
  </Preview.CSS>

  <Preview.Data>
    {`
          const tags = [
            { id: "t1", value: "feature" },
            { id: "t2", value: "fix" },
            { id: "t3", value: "bug" },
            { id: "t4", value: "docs" },
          ];
        `}
  </Preview.Data>
</Preview>
