# Autocomplete

Typed suggestions for free text, tags, and constrained single or multiple selection.

> For the complete documentation index, see [llms.txt](/llms.txt). Markdown variants are available by appending `.md` to any URL or sending an `Accept: text/markdown` header. An agent skill is available at [/.well-known/agent-skills/site-skill.md](/.well-known/agent-skills/site-skill.md).





## Playground [#playground]

```tsx
"use client";

import { useState } from "react";
import { Autocomplete } from "@/components/autocomplete";

type Framework = { id: string; label: string; group: string };
const items: readonly Framework[] = [
  {
    "group": "Libraries",
    "id": "react",
    "label": "React"
  },
  {
    "group": "Frameworks",
    "id": "vue",
    "label": "Vue"
  },
  {
    "group": "Frameworks",
    "id": "svelte",
    "label": "Svelte"
  }
];

export function AutocompleteDemo() {
  const [value, setValue] = useState<string>("");
  return (
    <Autocomplete
      items={items}
      getItemLabel={(item) => item.label}
      getItemValue={(item) => item.id}
      value={value}
      onValueChange={setValue}
      mode={"free-text"}
      multiple={false}
      label={"Framework"}
      labelStyle={"static"}
      placeholder={"Search frameworks"}
      size={"default"}
      disabled={false}
      readOnly={false}
      invalid={false}
      clearable={false}
      showTrigger={false}
      animated={true}
      autoHighlight={false}
      loading={false}
      loadingProps={{"variant":"arc","size":16}}
      backgroundLoading={false}
      className={"w-72 max-w-full"}
      inputProps={{ "aria-label": "Framework" }}
    />
  );
}
```

## Installation [#installation]

Initialize shadcn in your React project, then install this component from the Vandor UI registry:

```bash
# From the Vandor UI checkout, after initializing shadcn in the consumer:
pnpm registry:build
node scripts/install-component-family.cjs autocomplete /absolute/path/to/consumer
```

## Dependencies [#dependencies]

Installed automatically by the registry CLI. For manual setup:

```bash
npm install @base-ui/react@^1.8.0 cn@^0.4.0 lucide-react motion@^12.38.0
```

## Usage [#usage]

Labels use a transparent fieldset/legend notch over the existing surface. Floating labels remain raised while focus moves between the input, clear button, trigger, or chips; static labels sit above the field. The shared `field-outline.tsx` support file installs in your configured UI directory, not inside the Autocomplete family. CLI installation includes it; copying only the Source block does not.

`Autocomplete` assembles the field, suggestions, optional actions and chips. Strings need no accessors:

```tsx
"use client";

import { Autocomplete } from "@/components/autocomplete";

export function Demo() {
  return <Autocomplete items={["React", "Vue", "Svelte"]} label="Framework" />;
}
```

Use your configured UI alias if it differs. React 19, Tailwind CSS 4 and the standard shadcn theme tokens are required. Runtime dependencies are `@base-ui/react@^1.8.0`, `cn@^0.4.0`, `lucide-react`, and `motion@^12.38.0`. No website stylesheet or provider is imported. Use your consumer's background, input, foreground, accent, popover, border, ring, destructive and muted-foreground tokens; standard shadcn CSS supplies these. Popup motion is JavaScript-driven and requires no extra animation stylesheet.

Family installation supplies an `index.ts` UI entry, core modules, compact feedback
and pagination under your components alias. It also installs the public `Loading`
component's entire animation catalog and MIT license. Copying Source alone is
insufficient. Core does not require TanStack Query.

Stock shadcn 4.5.0 cannot preserve nested families under divergent aliases. From
this registry checkout, generate local artifacts and use the alias-aware wrapper:

```bash
pnpm registry:build
node scripts/install-component-family.cjs autocomplete /absolute/path/to/consumer
node scripts/install-component-family.cjs autocomplete-query /absolute/path/to/consumer
```

The optional second command requires your own Query provider. The wrapper uses
the real CLI, configured components/UI aliases and locally generated dependency
closure. Review overwrite prompts, especially when optional Query installs core
as a dependency. It never deletes old `ui/` copies. Transfer customizations and
remove obsolete copies deliberately. Consumer TypeScript aliases must resolve to
one in-project directory; ambiguous fallback paths are rejected, not guessed.

## Four value contracts [#four-value-contracts]

| Mode                  | Multiple | Committed value   | Empty  |
| --------------------- | -------- | ----------------- | ------ |
| `free-text` (default) | false    | `string`          | `""`   |
| `free-text`           | true     | `string[]`        | `[]`   |
| `selection`           | false    | original `Item`   | `null` |
| `selection`           | true     | original `Item[]` | `[]`   |

Literal mode/multiple settings infer callback types. Narrow runtime unions before rendering. `value` is committed application state; controlled props stay authoritative. `defaultValue` initializes uncontrolled state. Readonly item collections are supported and supplied arrays are never mutated.

### Free text [#free-text]

Typing updates the string immediately. Selecting a suggestion replaces it. Blur retains arbitrary text. There is no separate `inputValue` in single free-text mode.

<ComponentPreview name="autocomplete-demo" title="autocomplete-demo.tsx">
  <AutocompleteDemo />
</ComponentPreview>

### Free-text tags [#free-text-tags]

Enter commits a trimmed draft when no suggestion is highlighted. Empty strings and exact case-sensitive duplicates are rejected without callbacks. Choosing a suggestion follows the same rule. Commas and pasted text stay intact; blur and Tab never create tags. An uncommitted draft survives blur and Escape, but is not submitted.

<ComponentPreview src="examples/autocomplete-demo.tsx" title="autocomplete-demo.tsx">
  <AutocompleteFreeTextMultipleDemo />
</ComponentPreview>

### Single selection [#single-selection]

Objects require `getItemLabel` and `getItemValue`; the latter must return a unique stable string ID. Duplicate labels are allowed. Callbacks return the original current source item, not an ID. IDs compare newly fetched instances; an existing committed object remains displayable and submittable when absent from results.

Typing changes only `inputValue`, not the selection. Choosing replaces the committed item. Blur/Escape discards the query and restores the prior label (or empty input). Clear resets both. Controlled query owners must honor cleanup requests through `onInputValueChange`.

<ComponentPreview src="examples/autocomplete-demo.tsx" title="autocomplete-demo.tsx">
  <AutocompleteSelectionDemo />
</ComponentPreview>

### Multiple selection [#multiple-selection]

Only known items can be selected. New choices append in insertion order; duplicate IDs are rejected. Query cleanup never emits a value change. Blur/Escape discards the query, keeping all choices. Successful additions clear the query and keep the popup open.

<ComponentPreview src="examples/autocomplete-demo.tsx" title="autocomplete-demo.tsx">
  <AutocompleteSelectionMultipleDemo />
</ComponentPreview>

## Rich items and composition [#rich-items-and-composition]

`renderItem` changes visual content, not identity, search labels or submitted values. `groupBy` preserves first-seen group order and source order within each group. Empty groups are omitted. `isItemDisabled` prevents choosing a suggestion.

<ComponentPreview name="autocomplete-advanced-demo" title="autocomplete-advanced-demo.tsx">
  <AutocompleteAdvancedDemo />
</ComponentPreview>

Named parts: `AutocompleteRoot`, `AutocompleteInput`, `AutocompleteContent`, `AutocompleteList`, `AutocompleteItem`, `AutocompleteGroup`, `AutocompleteGroupLabel`, `AutocompleteEmpty`, `AutocompleteStatus`, `AutocompleteClear`, `AutocompleteTrigger`, `AutocompleteChips`, `AutocompleteChip`, `AutocompleteChipRemove`.

The Root uses the same four-mode contract, not an arbitrary Base UI pass-through. Provide `items` for identity/filtering. Pass original items to `AutocompleteItem.value`; put items in List and Group. Chips are supported only under `multiple` Root, inside `AutocompleteChips`, in committed-value order. `AutocompleteChip.label` is required; removal uses its label for the accessible name. In compound usage, supply your own associated label or input `aria-label`, and your layout/classes. Status and Empty are optional, mode-aware parts. Do not mix parts from unrelated primitive roots. Root's `groupBy`/`renderItem` do not assemble compound children; those are convenience assembly options.

## Async suggestions [#async-suggestions]

Applications own debounce, requests, cancellation, stale-response protection and caching. Set `filter={null}` for server-filtered results. Default filtering is case/accent-insensitive label matching; a custom `filter(item, query)` can replace it.

Error takes precedence over loading, then empty, then populated results. Loading/error suppress stale suggestion commits, but never erase choices or disable free-text edits/creation. Status feedback uses a polite region; no “no results” appears while loading/error is active. The component does not fetch or own retry logic.

<ComponentPreview name="autocomplete-async-demo" title="autocomplete-async-demo.tsx">
  <AutocompleteAsyncDemo />
</ComponentPreview>

This deterministic local service debounces for 300ms, aborts timers and ignores stale responses. Type `slow` to delay a result or `error` to see a predictable failure. Replace it with your own service, retaining cleanup.

## Optional Query integration [#optional-query-integration]

Import `useAutocompleteQuery` or `useAutocompleteInfiniteQuery` from
`@/components/autocomplete/query`. Install `@tanstack/react-query@^5.104.1` and
provide your own `QueryClientProvider`; no hidden provider is installed.

<ComponentPreview name="autocomplete-query-demo" title="autocomplete-query-demo.tsx">
  <AutocompleteQueryDemo />
</ComponentPreview>

Both hooks accept native options factories receiving debounced `search`, plus
`getItems`. Infinite `getItems` receives each selected page, including a page shape
changed by `select`, and object items require stable `getItemValue` IDs. Duplicate
IDs retain the first object and order without mutating cached pages. Include search
and every request identity in your query key.

`debounceMs` defaults to 300 and `minSearchLength` to 0. Fetching requires an open
popup, sufficient search length, settled debounce, hook `enabled`, and native
boolean/function `enabled` and `skipToken` semantics. UI Retry and Load more cannot
bypass these gates. Native Query results remain exposed as `query` for applications
making their own decisions. Invalid configuration throws `RangeError`.

Use `search/defaultSearch/onSearchChange` and `open/defaultOpen/onOpenChange` for
controlled or uncontrolled state. Selection and free-text tags bind only the query;
applications own committed `value/onValueChange`. Single free text binds search
as `value/onValueChange`, never `inputValue`; controlled `search` makes text
application-owned. Hook setters request changes without overriding controlled props.

Consume `signal` in query functions for Query cancellation. Closing the popup
alone does not guarantee abortion. Distinct keys prevent mixing search generations;
previous-key placeholders are not selectable. Same-search cached results remain
selectable during background refresh. `formatError` returns safe compact content;
raw Error objects are never rendered.

<ComponentPreview name="autocomplete-infinite-demo" title="autocomplete-infinite-demo.tsx">
  <AutocompleteInfiniteDemo />
</ComponentPreview>

The actual list viewport triggers automatic loading near its bottom only after
scrolling. Mounting a short list does not drain pages. An accessible `type="button"`
Load more remains available; page failure latches automatic retry until explicit
Retry or query change. Existing results and selections remain during page loading,
page errors and background refresh. Infinite scrolling is not virtualization.

| State                        | Compact presentation                     |
| ---------------------------- | ---------------------------------------- |
| Below minimum                | Hint, old options blocked                |
| Debouncing / initial fetch   | Public Loading and text                  |
| Initial error                | Error and Retry                          |
| Successful empty             | Dedicated empty message                  |
| Same-search background fetch | Retained options and Loading             |
| Next page fetching / error   | Retained options, footer Loading / Retry |
| More / exhausted             | Load more / no action                    |

Customize `loadingProps={{ variant: "dots", size: 16 }}`, `hintMessage`,
`loadingMessage`, `emptyMessage`, `retryLabel`, and pagination labels/messages.
Compound Root uses the same bindings with `AutocompleteFeedback` and
`AutocompletePagination`. Core feedback does not use registry ErrorState or Empty.
Playground pagination scenarios simulate states, not real API requests; the two
Query examples above use deterministic services and real Query caches.

## Forms and reset [#forms-and-reset]

Single free text submits its string; selection submits the stable ID. Multiple values submit repeated `name` entries in insertion order. Empty multiples submit no entries; empty singles submit `""`. Drafts never submit. `form` supports an external form ID. Disabled controls are excluded; read-only controls remain focusable and submitted.

`required` checks a committed selection/tag, not a typed query. The editable input receives validity feedback. Native reset restores uncontrolled defaults and initial drafts, closes the popup and resets transient state. Controlled state remains application-owned; the example's `onReset` explicitly restores its controlled value.

<ComponentPreview name="autocomplete-form-demo" title="autocomplete-form-demo.tsx">
  <AutocompleteFormDemo />
</ComponentPreview>

## Behavior & Accessibility [#behavior--accessibility]

### Inside Drawer and Dialog [#inside-drawer-and-dialog]

<ComponentPreview name="autocomplete-overlay-demo" title="autocomplete-overlay-demo.tsx">
  <AutocompleteOverlayDemo />
</ComponentPreview>

Install Button, Drawer and Dialog separately for this example. Suggestions keep their isolated overlay layer; selecting one updates the field without dismissing the parent. Use Tab/Shift+Tab, Escape and arrow keys to inspect focus and dismissal. Popup painting and collision need a real browser; DOM tests do not establish their appearance.

* Give the field a `label` or input ARIA name; placeholder alone is not a label. Static labels are default, floating labels opt in.
* Default height is 36px, `size="sm"` is 32px. Multiple chips expand and wrap the field. Popup width follows the anchor and its list scrolls within available viewport height.
* `className` owns the outer convenience field. `inputProps` owns native attributes, descriptions, handlers and ref; managed value/form/type/disabled/readOnly/required/size props are excluded. `contentProps` owns popup props and positioning/collision options, excluding children/render. Top-level ref targets the editable input and is merged with `inputProps.ref`.
* `invalid` marks the input; connect help/error text using `inputProps["aria-describedby"]`. Suggestion `error` is not automatically a validation error.
* Arrow keys navigate suggestions, Enter selects highlighted suggestions. A handled Enter does not submit the form. Ordinary single free-text Enter without a highlighted suggestion retains native submission behavior.
* IME Enter cannot select/create; the composition-ending Enter is guarded. No comma delimiters or paste splitting.
* Empty-draft Backspace focuses the last chip first. Delete/Backspace on that chip removes it and focuses the prior chip or input. Removal actions have accessible names and stay keyboard reachable.
* Read-only blocks choosing, clear, removal and value-changing shortcuts without excluding submission. Disabled blocks interaction and native submission.
* Consumer handlers are composed; `preventDefault()` cancels adapter behavior and underlying Base UI handlers. Public Base UI `preventBaseUIHandler()` is also supported on compound primitive events.
* `onValueChange` fires only on real edits/commits/removals/clear, never opening, highlighting, failed duplicates or query-only edits. Callbacks request controlled changes; no cancelable event-details contract is promised.
* `clearable` defaults false; trigger defaults on for selection, off for free text; `autoHighlight` defaults false. `animated` defaults true; popup fade/scale uses 180ms opening and 120ms closing, respecting reduced motion. Query edits are not animated.

Inline completion, virtualization, tag reordering and creating selection objects are not part of this release. Underlying [Base UI Autocomplete](https://base-ui.com/react/components/autocomplete) and [Combobox](https://base-ui.com/react/components/combobox) supply list navigation; Vandor `mode` is not Base UI's inline-completion mode.

## Portable Storybook stories [#portable-storybook-stories]

Install Autocomplete first, then optionally install its stories:

```bash
node scripts/install-component-family.cjs autocomplete-stories /absolute/path/to/consumer
```

This adds only colocated `autocomplete.stories.tsx` in the configured components family directory. It never reinstalls the component, adds/upgrades Storybook packages or modifies `.storybook/`. Supply matching Storybook React packages; verified types/composition target Storybook 10.6.1. Discover `**/*.stories.tsx`, import your own Tailwind/shadcn global stylesheet and theme in the preview, and retain your existing decorators. Static, floating, unlabeled and multiple-label stories are included; KeyboardSelection provides a reproducible manual keyboard flow, not simulated browser focus. No optional addon is required. Ordinary component installation is Storybook-free. Storybook UI not verified: this project has no Storybook server.

## Props [#props]

| Prop                | Type                                                                                                                      | Default                                | Description                                                                                      |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | ------------------------------------------------------------------------------------------------ |
| animated            | boolean                                                                                                                   | true                                   | Animated                                                                                         |
| autoHighlight       | boolean                                                                                                                   | false                                  | Auto highlight                                                                                   |
| className           | string                                                                                                                    | Not set                                | Convenience outer field container.                                                               |
| clearLabel          | string                                                                                                                    | "Clear value"                          | Accessible clear action name.                                                                    |
| clearable           | boolean                                                                                                                   | false                                  | Clearable                                                                                        |
| contentProps        | AutocompleteContentProps                                                                                                  | bottom, start, offset 4                | Popup attributes/ref/class/style and positioning/collision options; excludes children/render.    |
| defaultInputValue   | string                                                                                                                    | Selected label or empty                | Initial uncontrolled draft/query except single free text.                                        |
| defaultOpen         | boolean                                                                                                                   | false                                  | Initial popup state.                                                                             |
| defaultValue        | Same as value                                                                                                             | Mode-specific empty                    | Initial uncontrolled committed value restored on native reset.                                   |
| disabled            | boolean                                                                                                                   | false                                  | Disabled                                                                                         |
| emptyMessage        | ReactNode                                                                                                                 | "No results found."                    | Empty suggestion status.                                                                         |
| error               | ReactNode                                                                                                                 | Not set                                | Suggestion error; takes precedence over loading and empty.                                       |
| filter              | ((item: Item, query: string) => boolean) \| null                                                                          | Case/accent insensitive label matching | null disables filtering for server results.                                                      |
| form                | string                                                                                                                    | Nearest form                           | External native form ID, including reset ownership.                                              |
| getItemLabel        | (item: Item) => string                                                                                                    | String item                            | Required for objects; display and default filtering label.                                       |
| getItemValue        | (item: Item) => string                                                                                                    | String item                            | Required for objects; unique stable string identity and form value.                              |
| groupBy             | (item: Item) => string                                                                                                    | Not set                                | Convenience assembly: first-seen group order, original order inside groups.                      |
| grouping            | boolean                                                                                                                   | false                                  | Grouping                                                                                         |
| inputProps          | AutocompleteInputProps                                                                                                    | Not set                                | Native input attributes, handlers, descriptions and ref; excludes managed state/form/type props. |
| inputValue          | string                                                                                                                    | Not set                                | Controlled draft/query, forbidden for single free text.                                          |
| invalid             | boolean                                                                                                                   | false                                  | Invalid                                                                                          |
| isItemDisabled      | (item: Item) => boolean                                                                                                   | Not set                                | Disable an individual suggestion.                                                                |
| items               | readonly Item\[]                                                                                                          | Required                               | Flat source collection. Strings work without accessors; objects require both accessors.          |
| label               | string                                                                                                                    | Not set                                | Label                                                                                            |
| labelStyle          | "static" \| "floating"                                                                                                    | "static"                               | Label style                                                                                      |
| loading             | boolean                                                                                                                   | false                                  | Suppress stale result commits, preserve text editing and free-text creation.                     |
| loadingVariant      | "arc" \| "dots"                                                                                                           | "arc"                                  | Loading variant                                                                                  |
| automaticPagination | boolean                                                                                                                   | true                                   | Automatic pagination                                                                             |
| hasNextPage         | boolean                                                                                                                   | true                                   | Has next page                                                                                    |
| loadingMessage      | ReactNode                                                                                                                 | "Loading suggestions…"                 | Loading suggestion status.                                                                       |
| mode                | "free-text" \| "selection"                                                                                                | "free-text"                            | Mode                                                                                             |
| multiple            | boolean                                                                                                                   | false                                  | Multiple                                                                                         |
| name                | string                                                                                                                    | Not set                                | Committed form value, repeated entries for multiple; queries never submit.                       |
| onInputValueChange  | (query: string) => void                                                                                                   | Not set                                | Query edits and cleanup requests; never changes committed values.                                |
| onOpenChange        | (open: boolean) => void                                                                                                   | Not set                                | Popup change request.                                                                            |
| onValueChange       | (value) => void                                                                                                           | Not set                                | Real edits, commits, removals and clear; callback type follows literal mode/multiple.            |
| open                | boolean                                                                                                                   | Not set                                | Controlled popup state.                                                                          |
| placeholder         | string                                                                                                                    | Not set                                | Placeholder                                                                                      |
| readOnly            | boolean                                                                                                                   | false                                  | Read only                                                                                        |
| ref                 | Ref\<HTMLInputElement>                                                                                                    | Not set                                | Editable input, merged with inputProps.ref.                                                      |
| removeLabel         | (label: string) => string                                                                                                 | "Remove \{label}"                      | Accessible chip removal action name.                                                             |
| renderItem          | (item: Item) => ReactNode                                                                                                 | Item label                             | Convenience assembly: visual content only, not identity or filtering.                            |
| required            | boolean                                                                                                                   | false                                  | Requires committed value, not an uncommitted query.                                              |
| scenario            | "ready" \| "loading" \| "error" \| "empty" \| "hint" \| "background" \| "more" \| "page-loading" \| "page-error" \| "end" | "ready"                                | Status scenario                                                                                  |
| showTrigger         | boolean                                                                                                                   | true in selection; false in free-text  | Show trigger                                                                                     |
| size                | "default" \| "sm"                                                                                                         | "default"                              | Size                                                                                             |
| value               | string \| string\[] \| Item \| null \| Item\[]                                                                            | Mode-specific empty                    | Committed value. Controlled state stays authoritative.                                           |

Autocomplete owns its managed value/query/form state. inputProps forwards input attributes, handlers, ARIA descriptions and ref except managed state, form, disabled/readOnly/required, type and size. contentProps forwards popup attributes and listed positioning/collision options, excluding children/render. Compound parts use the same root contract; chips require multiple.

## Source [#source]

```tsx
import { cn } from "cn";
import type { ReactNode } from "react";

/** Accessible visual outline shared by labeled registry field triggers. */
export const FieldOutline = ({
  children,
  label,
  labelId,
  raised = false,
  staticLabel = false,
  className,
  htmlFor,
}: {
  children: ReactNode;
  label: string;
  labelId: string;
  raised?: boolean;
  staticLabel?: boolean;
  className?: string;
  htmlFor?: string;
}) => (
  <div
    className={cn("group/outline relative min-w-0", className)}
    data-field-outline=""
    data-raised={raised || staticLabel || undefined}
  >
    {label &&
      (staticLabel ? (
        <label
          htmlFor={htmlFor}
          id={labelId}
          className="mb-1.5 block text-sm font-medium"
        >
          {label}
        </label>
      ) : (
        <>
          <label
            id={labelId}
            htmlFor={htmlFor}
            data-floating={raised}
            className={cn(
              "pointer-events-none absolute z-10 max-w-[calc(100%-1rem)] -translate-y-1/2 truncate px-1 text-muted-foreground transition-[top,font-size] duration-150 motion-reduce:transition-none",
              raised
                ? "top-0 left-3 text-xs"
                : "top-1/2 left-3 text-sm group-focus-within/outline:top-0 group-focus-within/outline:text-xs"
            )}
          >
            {label}
          </label>
          <fieldset
            aria-hidden="true"
            className="pointer-events-none absolute inset-x-0 -top-1.5 bottom-0 min-w-0 rounded-md border border-input px-2 group-focus-within/outline:border-ring group-has-[[aria-invalid=true]]/outline:border-destructive group-has-[:disabled]/outline:opacity-50"
          >
            <legend
              className={cn(
                "invisible block h-3 max-w-0 overflow-hidden whitespace-nowrap p-0 text-xs leading-3 transition-[max-width] duration-150 group-focus-within/outline:max-w-full motion-reduce:transition-none",
                raised && "max-w-full"
              )}
            >
              <span className="px-1">{label}</span>
            </legend>
          </fieldset>
        </>
      ))}
    {children}
  </div>
);

```

## Credits [#credits]

* [ReUI](https://reui.io/docs/autocomplete): Design and API reference only. Vandor's adapter and styling are independently authored, not copied ReUI source.

* [Base UI](https://base-ui.com/react/components/combobox): Public autocomplete, combobox and chip primitives provide accessible interaction.
