# Autocomplete

Suggest completions while allowing free text.

Project search with suggestions

[Complete TSX file](/examples/autocomplete/default.tsx)

```tsx
"use client";

import { SearchIcon } from "lucide-react";
import {
  Autocomplete,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
} from "@/siteplane/components/autocomplete";

const projects = [
  "Website redesign",
  "Mobile app launch",
  "Billing migration",
  "Design system",
  "Customer onboarding",
  "Quarterly planning",
];

export default function Example() {
  return (
    <div className="w-full max-w-sm">
      <Autocomplete items={projects}>
        <AutocompleteInput
          aria-label="Search projects"
          placeholder="Search projects…"
          startAddon={<SearchIcon />}
        />
        <AutocompletePopup>
          <AutocompleteEmpty>No matching projects.</AutocompleteEmpty>
          <AutocompleteList>
            {(project: string) => (
              <AutocompleteItem key={project} value={project}>
                {project}
              </AutocompleteItem>
            )}
          </AutocompleteList>
        </AutocompletePopup>
      </Autocomplete>
    </div>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/autocomplete
```

With npm: `npx shadcn@4.18.0 add @siteplane/autocomplete`. [Registry JSON](/registry/autocomplete.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/form @siteplane/input
```

Field is installed with Input.

## Usage

```tsx
import {
  Autocomplete,
  AutocompleteInput,
  AutocompletePopup,
  AutocompleteEmpty,
  AutocompleteList,
  AutocompleteItem,
} from "@/siteplane/components/autocomplete";

<Autocomplete items={["Design"]}>
  <AutocompleteInput aria-label="Workspace" />
  <AutocompletePopup>
    <AutocompleteEmpty>No matching workspaces.</AutocompleteEmpty>
    <AutocompleteList>
      {(item: string) => (
        <AutocompleteItem key={item} value={item}>
          {item}
        </AutocompleteItem>
      )}
    </AutocompleteList>
  </AutocompletePopup>
</Autocomplete>;
```

## Composition

```text
Autocomplete
├── AutocompleteInput
└── AutocompletePopup
    ├── AutocompleteStatus (optional)
    ├── AutocompleteEmpty
    └── AutocompleteList → AutocompleteItem
```

## Controlled value and clear

The clear button updates the controlled value. Type a non-matching query to see the empty result.

Controlled value and clear

[Complete TSX file](/examples/autocomplete/controlled.tsx)

```tsx
"use client";

import {
  Autocomplete,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
} from "@/siteplane/components/autocomplete";
import { useState } from "react";

export default function Example() {
  const [value, setValue] = useState("");
  return (
    <div className="w-full max-w-sm">
      <Autocomplete
        items={["Design", "Engineering", "Research"]}
        value={value}
        onValueChange={setValue}
      >
        <AutocompleteInput
          aria-label="Workspace"
          placeholder="Find workspace"
          showClear
        />
        <AutocompletePopup>
          <AutocompleteEmpty>No matching workspaces.</AutocompleteEmpty>
          <AutocompleteList>
            {(item: string) => (
              <AutocompleteItem key={item} value={item}>
                {item}
              </AutocompleteItem>
            )}
          </AutocompleteList>
        </AutocompletePopup>
      </Autocomplete>
    </div>
  );
}
```

## Grouped suggestions

Pass groups as `items` and render each with `AutocompleteGroup`, `AutocompleteGroupLabel` and `AutocompleteCollection`. `itemToStringValue` turns an object item into the text that fills the input and drives filtering.

Tag suggestions in groups

[Complete TSX file](/examples/autocomplete/groups.tsx)

```tsx
"use client";

import { Fragment } from "react";
import {
  Autocomplete,
  AutocompleteCollection,
  AutocompleteEmpty,
  AutocompleteGroup,
  AutocompleteGroupLabel,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
  AutocompleteSeparator,
} from "@/siteplane/components/autocomplete";

type Tag = { id: string; label: string };

const tagGroups: { value: string; items: Tag[] }[] = [
  {
    value: "Status",
    items: [
      { id: "open", label: "Open" },
      { id: "in-progress", label: "In progress" },
      { id: "blocked", label: "Blocked" },
      { id: "done", label: "Done" },
    ],
  },
  {
    value: "Priority",
    items: [
      { id: "low", label: "Low" },
      { id: "medium", label: "Medium" },
      { id: "high", label: "High" },
      { id: "urgent", label: "Urgent" },
    ],
  },
  {
    value: "Team",
    items: [
      { id: "design", label: "Design" },
      { id: "frontend", label: "Frontend" },
      { id: "backend", label: "Backend" },
      { id: "support", label: "Support" },
    ],
  },
];

export default function Example() {
  return (
    <div className="w-full max-w-sm">
      <Autocomplete
        items={tagGroups}
        itemToStringValue={(tag: Tag) => tag.label}
      >
        <AutocompleteInput
          aria-label="Search tags"
          placeholder="Search tags…"
        />
        <AutocompletePopup>
          <AutocompleteEmpty>No tags found.</AutocompleteEmpty>
          <AutocompleteList>
            {(group: (typeof tagGroups)[number], index: number) => (
              <Fragment key={group.value}>
                <AutocompleteGroup items={group.items}>
                  <AutocompleteGroupLabel>{group.value}</AutocompleteGroupLabel>
                  <AutocompleteCollection>
                    {(tag: Tag) => (
                      <AutocompleteItem key={tag.id} value={tag}>
                        {tag.label}
                      </AutocompleteItem>
                    )}
                  </AutocompleteCollection>
                </AutocompleteGroup>
                {index < tagGroups.length - 1 && <AutocompleteSeparator />}
              </Fragment>
            )}
          </AutocompleteList>
        </AutocompletePopup>
      </Autocomplete>
    </div>
  );
}
```

## Limit results

Set `limit` to cap how many suggestions the list shows, and use `AutocompleteStatus` to say how many more match. `useAutocompleteFilter` returns the matching function the list uses by default, so the count stays in sync.

Skill suggestions with a result limit

[Complete TSX file](/examples/autocomplete/limit-results.tsx)

```tsx
"use client";

import { useMemo, useState } from "react";
import {
  Autocomplete,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
  AutocompleteStatus,
  useAutocompleteFilter,
} from "@/siteplane/components/autocomplete";

const limit = 6;

const skills = [
  "Accessibility",
  "Brand design",
  "Copywriting",
  "Customer research",
  "Data analysis",
  "Illustration",
  "Interaction design",
  "Motion design",
  "Product strategy",
  "Prototyping",
  "Usability testing",
  "User interviews",
  "Visual design",
  "Workshop facilitation",
];

export default function Example() {
  const [value, setValue] = useState("");
  const { contains } = useAutocompleteFilter();
  const matchCount = useMemo(
    () => skills.filter((skill) => contains(skill, value)).length,
    [contains, value],
  );
  const moreCount = Math.max(0, matchCount - limit);

  return (
    <div className="w-full max-w-sm">
      <Autocomplete
        items={skills}
        limit={limit}
        onValueChange={setValue}
        value={value}
      >
        <AutocompleteInput
          aria-label="Add a skill"
          placeholder="Add a skill…"
        />
        <AutocompletePopup>
          <AutocompleteEmpty>No matching skills.</AutocompleteEmpty>
          <AutocompleteList>
            {(skill: string) => (
              <AutocompleteItem key={skill} value={skill}>
                {skill}
              </AutocompleteItem>
            )}
          </AutocompleteList>
          {moreCount > 0 && (
            <AutocompleteStatus>
              {moreCount} more — keep typing to narrow the list
            </AutocompleteStatus>
          )}
        </AutocompletePopup>
      </Autocomplete>
    </div>
  );
}
```

## Variants and disabled

`variant` belongs to the input. Disabled prevents input interaction.

Autocomplete variants and disabled state

[Complete TSX file](/examples/autocomplete/appearance.tsx)

```tsx
"use client";

import {
  Autocomplete,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
} from "@/siteplane/components/autocomplete";
import { Field, FieldLabel } from "@/siteplane/components/field";

const workspaces = ["Design", "Engineering", "Research"];

const inputs: {
  disabled?: boolean;
  label: string;
  variant?: "default" | "elevated" | "inset";
}[] = [
  { label: "default", variant: "default" },
  { label: "elevated", variant: "elevated" },
  { label: "inset", variant: "inset" },
  { label: "disabled", disabled: true },
];

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      {inputs.map(({ disabled, label, variant }) => (
        <Field disabled={disabled} key={label}>
          <FieldLabel>Workspace, {label}</FieldLabel>
          <Autocomplete items={workspaces}>
            <AutocompleteInput placeholder="Find workspace" variant={variant} />
            <AutocompletePopup>
              <AutocompleteEmpty>No matching workspaces.</AutocompleteEmpty>
              <AutocompleteList>
                {(item: string) => (
                  <AutocompleteItem key={item} value={item}>
                    {item}
                  </AutocompleteItem>
                )}
              </AutocompleteList>
            </AutocompletePopup>
          </Autocomplete>
        </Field>
      ))}
    </div>
  );
}
```

## Sizes

`size` belongs to the input. Omit it to inherit the provider's control size.

Autocomplete sizes

[Complete TSX file](/examples/autocomplete/sizes.tsx)

```tsx
"use client";

import {
  Autocomplete,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
} from "@/siteplane/components/autocomplete";
import { Field, FieldLabel } from "@/siteplane/components/field";

const workspaces = ["Design", "Engineering", "Research"];

const sizes = [
  { size: "sm", label: "small" },
  { size: "md", label: "medium" },
  { size: "lg", label: "large" },
  { size: "xl", label: "extra large" },
] as const;

export default function Example() {
  return (
    <div className="grid w-full max-w-sm gap-4">
      {sizes.map(({ size, label }) => (
        <Field key={size}>
          <FieldLabel>Workspace, {label}</FieldLabel>
          <Autocomplete items={workspaces}>
            <AutocompleteInput placeholder="Find workspace" size={size} />
            <AutocompletePopup>
              <AutocompleteEmpty>No matching workspaces.</AutocompleteEmpty>
              <AutocompleteList>
                {(item: string) => (
                  <AutocompleteItem key={item} value={item}>
                    {item}
                  </AutocompleteItem>
                )}
              </AutocompleteList>
            </AutocompletePopup>
          </Autocomplete>
        </Field>
      ))}
    </div>
  );
}
```

## Form integration

Place the autocomplete in a `Field` with a `name`; the submitted value is the typed text, which can differ from every suggestion.

Job title field with suggestions

[Complete TSX file](/examples/autocomplete/form.tsx)

```tsx
"use client";

import {
  Autocomplete,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
} from "@/siteplane/components/autocomplete";
import { Button } from "@/siteplane/components/button";
import { Field, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";

const jobTitles = [
  "Product designer",
  "Product manager",
  "Frontend engineer",
  "Backend engineer",
  "Data analyst",
  "Customer success manager",
];

export default function Example() {
  return (
    <Form
      className="grid w-full max-w-sm gap-3"
      onSubmit={(event) => {
        event.preventDefault();
      }}
    >
      <Field name="jobTitle">
        <FieldLabel>Job title</FieldLabel>
        <Autocomplete items={jobTitles}>
          <AutocompleteInput placeholder="e.g. Product designer" />
          <AutocompletePopup>
            <AutocompleteEmpty>No suggestions.</AutocompleteEmpty>
            <AutocompleteList>
              {(title: string) => (
                <AutocompleteItem key={title} value={title}>
                  {title}
                </AutocompleteItem>
              )}
            </AutocompleteList>
          </AutocompletePopup>
        </Autocomplete>
      </Field>
      <Button type="submit">Save</Button>
    </Form>
  );
}
```

## API reference

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| AutocompleteInput.size | sm \| md \| lg \| xl \| number | provider / lg | Visual size, or numeric native input size. |
| AutocompleteInput.variant | default \| elevated \| inset | default | Input surface. |
| AutocompleteInput.showTrigger | boolean | false | Show popup toggle. |
| AutocompleteInput.showClear | boolean | false | Show clear action. |
| AutocompleteInput.startAddon | ReactNode | unset | Leading content. |
| AutocompleteInput.triggerProps / clearProps | Trigger.Props / Clear.Props | unset | Configure generated action buttons. |
| AutocompletePopup.side / align | Positioner side / alignment | bottom / start | Preferred position. |
| AutocompletePopup.sideOffset / alignOffset | offsets | 4 / unset | Gap and alignment correction. |
| AutocompletePopup.anchor | Positioner anchor | trigger/input | Custom anchor. |
| AutocompletePopup.portalProps | Portal.Props | unset | Portal options. |
| value / defaultValue | string | uncontrolled | Editable text, including arbitrary text. |
| onValueChange | (value, eventDetails) => void | unset | Value callback. |
| onOpenChange | (open, eventDetails) => void | unset | Popup visibility callback. |
| AutocompleteInput.unstyled | boolean | false | Remove the input surface. |

Autocomplete resets uncontrolled text when its associated native form resets. It does not add selected-item or multiple-selection semantics. `useAutocompleteFilter` is the foundation filtering hook. Input accepts foundation render and native ref contracts, unlike ComboboxInput’s narrower API.

AutocompleteList renders the filtered collection; Item represents a suggestion; Empty displays no results; Status provides status feedback. AutocompleteGroup, AutocompleteGroupLabel, AutocompleteSeparator and AutocompleteRow structure options. AutocompleteValue renders the current value; AutocompleteCollection supplies foundation collection composition. AutocompleteTrigger and AutocompleteClear are independently exported actions; their default render uses kit Buttons. Popup includes Portal and Positioner; AutocompleteList adds the ScrollArea. AutocompletePrimitive exports the namespace. Remaining root item-to-string/filter, keyboard, validation, disabled, readOnly, form, events and refs follow [Base UI Autocomplete](https://base-ui.com/react/components/autocomplete). Styled className props are strings. No independent fetch or loading service is supplied.

`AutocompleteTrigger` and `AutocompleteClear` accept their corresponding foundation props and default to a ghost Button with field-action profile and icon-xs size. Supply an accessible name when composing either action separately. `AutocompleteGroup`, `AutocompleteGroupLabel`, `AutocompleteRow`, `AutocompleteSeparator`, `AutocompleteValue`, and `AutocompleteCollection` accept their matching foundation part props with kit styling; they add no owned state API.

[Read the installed source](/source/autocomplete).

## Usage notes

Arrow keys navigate suggestions, Enter accepts and Escape dismisses. Keep a visible label in a production form. For async data, the application owns fetching, stale-result handling and pending/error status; render those states through Status and the surrounding field. See [Choosing selection controls](/docs/guides/selection/).
