# Autocomplete

> Help people complete typed input with relevant suggestions.

Source: https://www.honestui.com/docs/components/autocomplete

```tsx
"use client"

import {
  Autocomplete,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
} from "@/components/honest-ui/ui/autocomplete"

const items = [
  { label: "Apple", value: "apple" },
  { label: "Banana", value: "banana" },
  { label: "Orange", value: "orange" },
  { label: "Grape", value: "grape" },
  { label: "Strawberry", value: "strawberry" },
  { label: "Mango", value: "mango" },
  { label: "Pineapple", value: "pineapple" },
  { label: "Kiwi", value: "kiwi" },
  { label: "Peach", value: "peach" },
  { label: "Pear", value: "pear" },
]

export function AutocompleteDemo() {
  return (
    <div className="w-full max-w-64">
      <Autocomplete items={items}>
        <AutocompleteInput
          placeholder="Search items…"
          aria-label="Search items"
        />
        <AutocompletePopup>
          <AutocompleteEmpty>No items found.</AutocompleteEmpty>
          <AutocompleteList>
            {(item) => (
              <AutocompleteItem key={item.value} value={item}>
                {item.label}
              </AutocompleteItem>
            )}
          </AutocompleteList>
        </AutocompletePopup>
      </Autocomplete>
    </div>
  )
}

```

## Overview [#overview]

Autocomplete speeds up typing that has a known shape but not a closed list: addresses, cities, job titles, internal page names. The person can always type something no suggestion anticipated — the list advises, it does not decide.

That is the boundary with Combobox:

* **The value must be one of your options** — Combobox. It rejects anything else by design.
* **Free text, helped along** — Autocomplete. This component.
* **Many free-form values in one field** — Tags Input.

If you find yourself validating that autocomplete input "matches one of the options", you wanted a combobox; if you find yourself fighting a combobox to accept custom values, you wanted this.

## Anatomy [#anatomy]

The field is an input plus a popup of suggestions. Parts mirror the combobox family: `AutocompleteInput`, `AutocompletePopup`, `AutocompleteList`, `AutocompleteItem`, `AutocompleteEmpty`, `AutocompleteGroup(Label)`, `AutocompleteStatus` for loading and result counts, `AutocompleteClear`, and `AutocompleteTrigger` when it should open on click like a select.

## Behavior [#behavior]

**Keyboard.**

| Key                                       | Result                                       |
| ----------------------------------------- | -------------------------------------------- |
| Character keys                            | Type freely; the list filters as suggestions |
| <kbd>ArrowDown</kbd> / <kbd>ArrowUp</kbd> | Highlight a suggestion                       |
| <kbd>Enter</kbd>                          | Accept the highlighted suggestion            |
| <kbd>Escape</kbd>                         | Dismiss the popup — typed text stays         |
| <kbd>Tab</kbd>                            | Leave the field as typed                     |

The critical difference from select-family controls: dismissing never destroys the typed value. Suggestions accelerate input; they are never a gate on it.

**Async suggestions.** Fetch on change, keep the previous results visible while new ones load, and report state through `AutocompleteStatus`. Debounce on your side; the component re-renders cheaply but your API is not free.

**Grouping.** Group suggestions when they mix kinds — recent searches next to full results, or cities grouped by country. Group labels are announced by screen readers.

## States [#states]

While loading, say so via `AutocompleteStatus`; while empty, `AutocompleteEmpty` should distinguish "nothing matches" from "still fetching" whenever you can. Invalid state follows `aria-invalid` with the danger border and pairs with an error message via Field. Disabled skips focus and submission entirely.

Long suggestions wrap inside the popup; long *typed* values scroll horizontally in the input as any text input does. Tokens handle dark mode and RTL mirroring automatically.

## Accessibility [#accessibility]

The visible label names the input. The popup is a real listbox: screen-reader users hear how many suggestions matched, which is highlighted, and can ignore the popup entirely without losing their typed text. Because the field's whole model is "typed text is the truth", nothing in the suggestion UI overwrites input silently — acceptance happens only through explicit selection.

Suggestion copy matters more than usual here: people scan fragments mid-typing. Put the distinguishing part of each label first, and include enough context ("Springfield, Illinois") that similar entries are tellable apart.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;autocomplete&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/autocomplete.tsx

```tsx
"use client"

import { Autocomplete as AutocompletePrimitive } from "@base-ui-components/react/autocomplete"
import { ChevronsUpDown as ChevronsUpDownIcon, X as XIcon } from "honestui/icons"

import { cn } from "@/lib/utils"
import { Input } from "@/components/honest-ui/ui/input"
import { ScrollArea } from "@/components/honest-ui/ui/scroll-area"

const Autocomplete = AutocompletePrimitive.Root

function AutocompleteInput({
  className,
  showTrigger = false,
  showClear = false,
  size,
  ...props
}: Omit<AutocompletePrimitive.Input.Props, "size"> & {
  showTrigger?: boolean
  showClear?: boolean
  size?: "sm" | "default" | "lg" | number
}) {
  const sizeValue = (size ?? "default") as "sm" | "default" | "lg" | number

  return (
    <div className="relative w-full">
      <AutocompletePrimitive.Input
        data-slot="autocomplete-input"
        className={cn(
          sizeValue === "sm"
            ? "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-6.5"
            : "has-[+[data-slot=autocomplete-trigger],+[data-slot=autocomplete-clear]]:*:data-[slot=autocomplete-input]:pe-7",
          className
        )}
        render={<Input size={sizeValue} />}
        {...props}
      />
      {showTrigger && (
        <AutocompleteTrigger
          className={cn(
            "absolute top-1/2 inline-flex size-7 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-72 transition-colors outline-none hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
            sizeValue === "sm" ? "end-0" : "end-0.5"
          )}
        >
          <ChevronsUpDownIcon />
        </AutocompleteTrigger>
      )}
      {showClear && (
        <AutocompleteClear
          className={cn(
            "absolute top-1/2 inline-flex size-7 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-72 transition-colors outline-none hover:opacity-100 has-[+[data-slot=autocomplete-clear]]:hidden pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
            sizeValue === "sm" ? "end-0" : "end-0.5"
          )}
        >
          <XIcon />
        </AutocompleteClear>
      )}
    </div>
  )
}

function AutocompletePopup({
  className,
  children,
  sideOffset = 4,
  ...props
}: AutocompletePrimitive.Popup.Props & {
  sideOffset?: number
}) {
  return (
    <AutocompletePrimitive.Portal>
      <AutocompletePrimitive.Positioner
        data-slot="autocomplete-positioner"
        className="z-50 select-none"
        sideOffset={sideOffset}
      >
        <span className="relative flex max-h-full origin-(--transform-origin) rounded-lg border bg-popover bg-clip-padding transition-[scale,opacity] before:pointer-events-none before:absolute before:inset-0 before:rounded-[calc(var(--radius-lg)-1px)] before:shadow-lg has-data-starting-style:scale-98 has-data-starting-style:opacity-0 dark:not-in-data-[slot=group]:bg-clip-border">
          <AutocompletePrimitive.Popup
            data-slot="autocomplete-popup"
            className={cn(
              "flex max-h-[min(var(--available-height),23rem)] w-(--anchor-width) max-w-(--available-width) flex-col",
              className
            )}
            {...props}
          >
            {children}
          </AutocompletePrimitive.Popup>
        </span>
      </AutocompletePrimitive.Positioner>
    </AutocompletePrimitive.Portal>
  )
}

function AutocompleteItem({
  className,
  children,
  ...props
}: AutocompletePrimitive.Item.Props) {
  return (
    <AutocompletePrimitive.Item
      data-slot="autocomplete-item"
      className={cn(
        "flex cursor-default items-center rounded-sm px-2 py-1 text-base outline-none select-none data-disabled:pointer-events-none data-disabled:opacity-64 data-highlighted:bg-accent data-highlighted:text-accent-foreground sm:text-sm",
        className
      )}
      {...props}
    >
      {children}
    </AutocompletePrimitive.Item>
  )
}

function AutocompleteSeparator({
  className,
  ...props
}: AutocompletePrimitive.Separator.Props) {
  return (
    <AutocompletePrimitive.Separator
      className={cn("mx-2 my-1 h-px bg-border last:hidden", className)}
      data-slot="autocomplete-separator"
      {...props}
    />
  )
}

function AutocompleteGroup({
  className,
  ...props
}: AutocompletePrimitive.Group.Props) {
  return (
    <AutocompletePrimitive.Group
      data-slot="autocomplete-group"
      className={className}
      {...props}
    />
  )
}

function AutocompleteGroupLabel({
  className,
  ...props
}: AutocompletePrimitive.GroupLabel.Props) {
  return (
    <AutocompletePrimitive.GroupLabel
      className={cn(
        "px-2 py-1.5 text-xs font-medium text-muted-foreground",
        className
      )}
      data-slot="autocomplete-group-label"
      {...props}
    />
  )
}

function AutocompleteEmpty({
  className,
  ...props
}: AutocompletePrimitive.Empty.Props) {
  return (
    <AutocompletePrimitive.Empty
      className={cn(
        "text-center text-sm text-muted-foreground not-empty:p-2",
        className
      )}
      data-slot="autocomplete-empty"
      {...props}
    />
  )
}

function AutocompleteRow({
  className,
  ...props
}: AutocompletePrimitive.Row.Props) {
  return (
    <AutocompletePrimitive.Row
      data-slot="autocomplete-row"
      className={className}
      {...props}
    />
  )
}

function AutocompleteValue({ ...props }: AutocompletePrimitive.Value.Props) {
  return (
    <AutocompletePrimitive.Value data-slot="autocomplete-value" {...props} />
  )
}

function AutocompleteList({
  className,
  ...props
}: AutocompletePrimitive.List.Props) {
  return (
    <ScrollArea className="flex-1">
      <AutocompletePrimitive.List
        data-slot="autocomplete-list"
        className={cn(
          "not-empty:scroll-py-1 not-empty:px-1 not-empty:py-1 in-data-has-overflow-y:pe-3",
          className
        )}
        {...props}
      />
    </ScrollArea>
  )
}

function AutocompleteClear({
  className,
  ...props
}: AutocompletePrimitive.Clear.Props) {
  return (
    <AutocompletePrimitive.Clear
      data-slot="autocomplete-clear"
      className={cn(
        "absolute end-0.5 top-1/2 inline-flex size-7 shrink-0 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md border border-transparent opacity-72 transition-[color,background-color,box-shadow,opacity] outline-none hover:opacity-100 pointer-coarse:after:absolute pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
        className
      )}
      {...props}
    >
      <XIcon />
    </AutocompletePrimitive.Clear>
  )
}

function AutocompleteStatus({
  className,
  ...props
}: AutocompletePrimitive.Status.Props) {
  return (
    <AutocompletePrimitive.Status
      data-slot="autocomplete-status"
      className={cn(
        "px-3 py-2 text-xs font-medium text-muted-foreground empty:m-0 empty:p-0",
        className
      )}
      {...props}
    />
  )
}

function AutocompleteCollection({
  ...props
}: AutocompletePrimitive.Collection.Props) {
  return (
    <AutocompletePrimitive.Collection
      data-slot="autocomplete-collection"
      {...props}
    />
  )
}

function AutocompleteTrigger({
  className,
  ...props
}: AutocompletePrimitive.Trigger.Props) {
  return (
    <AutocompletePrimitive.Trigger
      data-slot="autocomplete-trigger"
      className={className}
      {...props}
    />
  )
}

export {
  Autocomplete,
  AutocompleteInput,
  AutocompleteTrigger,
  AutocompletePopup,
  AutocompleteItem,
  AutocompleteSeparator,
  AutocompleteGroup,
  AutocompleteGroupLabel,
  AutocompleteEmpty,
  AutocompleteValue,
  AutocompleteList,
  AutocompleteClear,
  AutocompleteStatus,
  AutocompleteRow,
  AutocompleteCollection,
}

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

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

```tsx
<Autocomplete items={cities}>
  <AutocompletePopup>
    <AutocompleteInput placeholder="Your city" />
    <AutocompleteList>
      {(item) => (
        <AutocompleteItem key={item.value} value={item}>
          {item.label}
        </AutocompleteItem>
      )}
    </AutocompleteList>
    <AutocompleteEmpty>No matches — you can still use your city.</AutocompleteEmpty>
  </AutocompletePopup>
</Autocomplete>
```

## Don't do this [#dont-do-this]

### Gatekeeping free text [#gatekeeping-free-text]

```tsx
// Bad
// rejecting submit unless the typed value equals some suggestion
if (!options.includes(input)) setError("Choose one of the listed options");
```

```tsx
// Good
<Combobox items={options} /> // closed list, honestly enforced
```

An autocomplete that refuses non-suggested values is a combobox with worse UX — the person fought the field to enter something legitimate (a new store location) and lost. If the set is truly closed, use the control whose contract says so; if it is open, let typed values through and validate on meaning.

### Blank popups during fetches [#blank-popups-during-fetches]

```tsx
// Bad
{results.length > 0 && <AutocompleteList>…</AutocompleteList>}
// the whole list vanishes mid-load
```

```tsx
// Good
<AutocompleteStatus>
  {loading ? "Searching…" : `${results.length} matches`}
</AutocompleteStatus>
```

When suggestions disappear without explanation, people stop typing to wait or assume their query found nothing and rephrase it. Status text at the top of the popup keeps the feedback loop honest during fetches.

## Examples [#examples]

### Async suggestions [#async-suggestions]

```tsx
"use client"

import * as React from "react"
import { Autocomplete as AutocompletePrimitive } from "@base-ui-components/react/autocomplete"
import { LoaderCircle as LoaderCircleIcon } from "honestui/icons"

import {
  Autocomplete,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
  AutocompleteStatus,
} from "@/components/honest-ui/ui/autocomplete"

type Movie = { id: string; title: string; year: number }
const top100Movies: Movie[] = [
  { 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: "The Godfather Part II", year: 1974 },
  { id: "5", title: "12 Angry Men", year: 1957 },
  { id: "8", title: "Pulp Fiction", year: 1994 },
  { id: "11", title: "Forrest Gump", year: 1994 },
  { id: "14", title: "Inception", year: 2010 },
]

async function searchMovies(
  query: string,
  filter: (item: string, query: string) => boolean
): Promise<Movie[]> {
  await new Promise((resolve) => setTimeout(resolve, 350))
  if (query === "will_error") {
    throw new Error("Network error")
  }
  return top100Movies.filter(
    (movie) =>
      filter(movie.title, query) || filter(movie.year.toString(), query)
  )
}

export function AutocompleteAsync() {
  const [searchValue, setSearchValue] = React.useState("")
  const [isLoading, setIsLoading] = React.useState(false)
  const [searchResults, setSearchResults] = React.useState<Movie[]>([])
  const [error, setError] = React.useState<string | null>(null)

  const { contains } = AutocompletePrimitive.useFilter({ sensitivity: "base" })

  React.useEffect(() => {
    if (!searchValue) {
      return
    }

    let ignore = false

    const timeoutId = setTimeout(async () => {
      try {
        const results = await searchMovies(searchValue, contains)
        if (!ignore) setSearchResults(results)
      } catch {
        if (!ignore) {
          setError("Failed to fetch movies. Please try again.")
          setSearchResults([])
        }
      } finally {
        if (!ignore) setIsLoading(false)
      }
    }, 300)

    return () => {
      clearTimeout(timeoutId)
      ignore = true
    }
  }, [searchValue, contains])

  let status: React.ReactNode = `${searchResults.length} result${searchResults.length === 1 ? "" : "s"} found`
  if (isLoading) {
    status = (
      <span className="flex items-center justify-between gap-2 text-muted-foreground">
        Searching...
        <LoaderCircleIcon className="size-4 animate-spin" aria-hidden />
      </span>
    )
  } else if (error) {
    status = (
      <span className="text-sm font-normal text-destructive">{error}</span>
    )
  } else if (searchResults.length === 0 && searchValue) {
    status = (
      <span className="text-sm font-normal text-muted-foreground">
        Movie or year “{searchValue}” does not exist in the Top
        100 IMDb movies
      </span>
    )
  }

  const shouldRenderPopup = searchValue !== ""

  return (
    <div className="w-full max-w-64">
      <Autocomplete
        items={searchResults}
        value={searchValue}
        onValueChange={(value) => {
          setSearchValue(value)
          setIsLoading(Boolean(value))
          setError(null)
          if (!value) setSearchResults([])
        }}
        itemToStringValue={(item: unknown) => (item as Movie).title}
        filter={null}
      >
        <AutocompleteInput placeholder="e.g. Pulp Fiction or 1994" />
        {shouldRenderPopup && (
          <AutocompletePopup aria-busy={isLoading || undefined}>
            <AutocompleteStatus className="text-muted-foreground">
              {status}
            </AutocompleteStatus>
            <AutocompleteList>
              {(movie: Movie) => (
                <AutocompleteItem key={movie.id} value={movie}>
                  <div className="flex w-full flex-col gap-1">
                    <div className="font-medium">{movie.title}</div>
                    <div className="text-xs text-muted-foreground">
                      {movie.year}
                    </div>
                  </div>
                </AutocompleteItem>
              )}
            </AutocompleteList>
          </AutocompletePopup>
        )}
      </Autocomplete>
    </div>
  )
}

```

### Grouped results [#grouped-results]

```tsx
"use client"

import * as React from "react"

import {
  Autocomplete,
  AutocompleteCollection,
  AutocompleteEmpty,
  AutocompleteGroup,
  AutocompleteGroupLabel,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
  AutocompleteSeparator,
} from "@/components/honest-ui/ui/autocomplete"

// Grouped items demo
type Tag = { id: string; label: string; group: "Status" | "Priority" | "Team" }
type TagGroup = { value: string; items: Tag[] }

const tagsData: Tag[] = [
  // Status
  { id: "s-open", label: "Open", group: "Status" },
  { id: "s-in-progress", label: "In progress", group: "Status" },
  { id: "s-blocked", label: "Blocked", group: "Status" },
  { id: "s-resolved", label: "Resolved", group: "Status" },
  { id: "s-closed", label: "Closed", group: "Status" },
  // Priority
  { id: "p-low", label: "Low", group: "Priority" },
  { id: "p-medium", label: "Medium", group: "Priority" },
  { id: "p-high", label: "High", group: "Priority" },
  { id: "p-urgent", label: "Urgent", group: "Priority" },
  // Team
  { id: "t-design", label: "Design", group: "Team" },
  { id: "t-frontend", label: "Frontend", group: "Team" },
  { id: "t-backend", label: "Backend", group: "Team" },
  { id: "t-devops", label: "DevOps", group: "Team" },
  { id: "t-qa", label: "QA", group: "Team" },
  { id: "t-mobile", label: "Mobile", group: "Team" },
  { id: "t-data", label: "Data", group: "Team" },
  { id: "t-security", label: "Security", group: "Team" },
  { id: "t-platform", label: "Platform", group: "Team" },
  { id: "t-infra", label: "Infrastructure", group: "Team" },
  { id: "t-product", label: "Product", group: "Team" },
  { id: "t-marketing", label: "Marketing", group: "Team" },
  { id: "t-sales", label: "Sales", group: "Team" },
  { id: "t-support", label: "Support", group: "Team" },
  { id: "t-research", label: "Research", group: "Team" },
  { id: "t-content", label: "Content", group: "Team" },
  { id: "t-analytics", label: "Analytics", group: "Team" },
  { id: "t-operations", label: "Operations", group: "Team" },
  { id: "t-finance", label: "Finance", group: "Team" },
  { id: "t-hr", label: "HR", group: "Team" },
  { id: "t-legal", label: "Legal", group: "Team" },
  { id: "t-growth", label: "Growth", group: "Team" },
  { id: "t-partner", label: "Partner", group: "Team" },
  { id: "t-community", label: "Community", group: "Team" },
  { id: "t-docs", label: "Docs", group: "Team" },
  { id: "t-l10n", label: "Localization", group: "Team" },
  { id: "t-a11y", label: "Accessibility", group: "Team" },
  { id: "t-sre", label: "SRE", group: "Team" },
  { id: "t-release", label: "Release", group: "Team" },
  { id: "t-architecture", label: "Architecture", group: "Team" },
  { id: "t-ux", label: "UX", group: "Team" },
  { id: "t-ui", label: "UI", group: "Team" },
  { id: "t-management", label: "Management", group: "Team" },
]

function groupTags(tags: Tag[]): TagGroup[] {
  const groups: Record<string, Tag[]> = {}
  for (const t of tags) {
    ;(groups[t.group] ??= []).push(t)
  }
  const order: Array<TagGroup["value"]> = ["Status", "Priority", "Team"]
  return order.map((value) => ({ value, items: groups[value] ?? [] }))
}

const groupedTags: TagGroup[] = groupTags(tagsData)

export function AutocompleteGrouped() {
  return (
    <div className="w-full max-w-64">
      <Autocomplete items={groupedTags}>
        <div className="flex flex-col items-start gap-2">
          <AutocompleteInput
            placeholder="e.g. feature"
            aria-label="Search tags"
          />
        </div>
        <AutocompletePopup>
          <AutocompleteEmpty>No tags found.</AutocompleteEmpty>
          <AutocompleteList>
            {(group: TagGroup) => (
              <React.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>
                {group.value !== "Team" && <AutocompleteSeparator />}
              </React.Fragment>
            )}
          </AutocompleteList>
        </AutocompletePopup>
      </Autocomplete>
    </div>
  )
}

```

### With clear button [#with-clear-button]

```tsx
"use client"

import {
  Autocomplete,
  AutocompleteEmpty,
  AutocompleteInput,
  AutocompleteItem,
  AutocompleteList,
  AutocompletePopup,
} from "@/components/honest-ui/ui/autocomplete"

const items = [
  { label: "Apple", value: "apple" },
  { label: "Banana", value: "banana" },
  { label: "Orange", value: "orange" },
  { label: "Grape", value: "grape" },
  { label: "Strawberry", value: "strawberry" },
  { label: "Mango", value: "mango" },
  { label: "Pineapple", value: "pineapple" },
  { label: "Kiwi", value: "kiwi" },
  { label: "Peach", value: "peach" },
  { label: "Pear", value: "pear" },
]

export function AutocompleteWithClear() {
  return (
    <div className="w-full max-w-64">
      <Autocomplete items={items}>
        <AutocompleteInput
          placeholder="Search items…"
          aria-label="Search items"
          showClear
        />
        <AutocompletePopup>
          <AutocompleteEmpty>No items found.</AutocompleteEmpty>
          <AutocompleteList>
            {(item) => (
              <AutocompleteItem key={item.value} value={item}>
                {item.label}
              </AutocompleteItem>
            )}
          </AutocompleteList>
        </AutocompletePopup>
      </Autocomplete>
    </div>
  )
}

```

### In a form [#in-a-form]

```tsx
"use client";

import * as React from "react";

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

const items = [
  { label: "Apple", value: "apple" },
  { label: "Banana", value: "banana" },
  { label: "Orange", value: "orange" },
  { label: "Grape", value: "grape" },
  { label: "Strawberry", value: "strawberry" },
  { label: "Mango", value: "mango" },
  { label: "Pineapple", value: "pineapple" },
  { label: "Kiwi", value: "kiwi" },
  { label: "Peach", value: "peach" },
  { label: "Pear", value: "pear" },
];

export function AutocompleteForm() {
  const [status, setStatus] = React.useState("");
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const selectedItem = formData.get("item");
    // Base UI extracts the 'label' property from objects, so we need to find the corresponding value
    const itemValue =
      items.find((item) => item.label === selectedItem)?.value || selectedItem;
    setStatus(`Submitted favorite: ${itemValue || "the selected item"}.`);
  };

  return (
    <Form onSubmit={onSubmit} className="grid w-full max-w-64 gap-4">
      <Field>
        <FieldLabel>Favorite item</FieldLabel>
        <Autocomplete items={items} name="item" required>
          <AutocompleteInput placeholder="Search items…" />
          <AutocompletePopup>
            <AutocompleteEmpty>No items found.</AutocompleteEmpty>
            <AutocompleteList>
              {(item) => (
                <AutocompleteItem key={item.value} value={item}>
                  {item.label}
                </AutocompleteItem>
              )}
            </AutocompleteList>
          </AutocompletePopup>
        </Autocomplete>
        <FieldError>Select an item.</FieldError>
      </Field>
      <Button type="submit">Save favorite</Button>
      <p className="text-sm text-muted-foreground" role="status">
        {status}
      </p>
    </Form>
  );
}

```

## API reference [#api-reference]

`Autocomplete` accepts Base UI Autocomplete props:

| Prop                     | Values                                | Default |
| ------------------------ | ------------------------------------- | ------- |
| `items`                  | `{ label: string; value: unknown }[]` | —       |
| `value` / `defaultValue` | item                                  | —       |

Parts: `AutocompleteInput`, `AutocompleteTrigger`, `AutocompletePopup`, `AutocompleteList`, `AutocompleteItem`, `AutocompleteGroup(Label)`, `AutocompleteEmpty`, `AutocompleteStatus`, `AutocompleteClear`, `AutocompleteValue`.

See the [Base UI Autocomplete API](https://base-ui.com/react/components/autocomplete#api-reference).
