Autocomplete

A text input with suggestions that filter as you type. Unlike a select or combobox, autocomplete accepts freeform text and treats suggestions as quick shortcuts.

Default

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

const tags = [
  { value: "rocket", label: "Rocket" },
  { value: "orbit-bike", label: "Orbit Bike" },
  { value: "lunar-car", label: "Lunar Car" },
  { value: "telemetry", label: "Telemetry" },
]

export default function Example() {
  return (
    <Autocomplete.Root items={tags}>
      <label className="flex w-64 flex-col gap-2 text-sm font-medium">
        Search the hangar
        <Autocomplete.Input placeholder="E.g. rocket" />
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner>
          <Autocomplete.Popup>
            <Autocomplete.Empty>Nothing in the hangar matches.</Autocomplete.Empty>
            <Autocomplete.List>
              {(tag) => (
                <Autocomplete.Item key={tag.value} value={tag}>
                  {tag.label}
                </Autocomplete.Item>
              )}
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

Usage Guidelines

  • Autocomplete vs. Combobox: Use Autocomplete when the user can type anything and suggestions are strictly optional shortcuts. Use Combobox when the user must pick from a predefined set of options or when you need to track a separate selected ID.
  • Accessible labels: Always associate the input with an accessible name by wrapping it in a <label> or providing an aria-label.
  • Keyboard controls: Use ↓ / ↑ to move highlight through items, Enter to choose the active item, Escape to dismiss the popup, and Tab to close the menu and advance focus.
  • Working with object arrays: If your items are objects with { value, label }, the component automatically uses the label. If your data uses custom keys like { id, title }, pass itemToStringValue={(item) => item.title} so the selected item writes cleanly into the input.
  • Async and remote search: Set filter={null} to disable internal filtering, then pass your debounced search results to filteredItems. Use Autocomplete.Status to announce loading states or match counts to assistive technologies.
  • Custom matching: Pass a custom function to filter if you need fuzzy matching, multi-word matching, or custom accent folding.
  • Always keep Empty and Status mounted: Conditionally render the children inside Autocomplete.Empty and Autocomplete.Status, not the component elements themselves. This keeps the ARIA live region registered in the DOM so screen readers announce changes.

More detail in the Base UI Autocomplete docs.

Anatomy

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

<Autocomplete.Root>
  <Autocomplete.InputGroup>
    <Autocomplete.Input />
    <Autocomplete.Trigger />
    <Autocomplete.Icon />
    <Autocomplete.Clear />
    <Autocomplete.Value />
  </Autocomplete.InputGroup>

  <Autocomplete.Portal>
    <Autocomplete.Backdrop />
    <Autocomplete.Positioner>
      <Autocomplete.Popup>
        <Autocomplete.Arrow />

        <Autocomplete.Status />
        <Autocomplete.Empty />

        <Autocomplete.List>
          <Autocomplete.Row>
            <Autocomplete.Item />
          </Autocomplete.Row>

          <Autocomplete.Separator />

          <Autocomplete.Group>
            <Autocomplete.GroupLabel />
          </Autocomplete.Group>

          <Autocomplete.Collection />
        </Autocomplete.List>
      </Autocomplete.Popup>
    </Autocomplete.Positioner>
  </Autocomplete.Portal>
</Autocomplete.Root>

With icon and clear button

Put a search icon and clear control inside Autocomplete.InputGroup, which shares one focus ring with the input.

import { SearchIcon } from "lucide-react"
import { Autocomplete } from "@/components/ui/autocomplete"

const tags = [
  { value: "rocket", label: "Rocket" },
  { value: "orbit-bike", label: "Orbit Bike" },
  { value: "lunar-car", label: "Lunar Car" },
]

export default function Example() {
  return (
    <Autocomplete.Root items={tags}>
      <label className="flex w-64 flex-col gap-2 text-sm font-medium">
        Search the hangar
        <Autocomplete.InputGroup>
          <SearchIcon
            className="size-4 shrink-0 text-muted-foreground"
            aria-hidden
          />
          <Autocomplete.Input placeholder="E.g. rocket" />
          <Autocomplete.Clear />
        </Autocomplete.InputGroup>
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner>
          <Autocomplete.Popup>
            <Autocomplete.Empty>Nothing in the hangar matches.</Autocomplete.Empty>
            <Autocomplete.List>
              {(tag) => (
                <Autocomplete.Item key={tag.value} value={tag}>
                  {tag.label}
                </Autocomplete.Item>
              )}
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

Grouped items

Pass groups shaped like { value, items: [] }, and render each with Autocomplete.Group, a label, and Autocomplete.Collection for its items.

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

// Groups are objects with an items array. Extra fields like value
// become the group label when you render GroupLabel.
const groupedTags = [
  {
    value: "Vehicles",
    items: [
      { value: "rocket", label: "Rocket" },
      { value: "orbit-bike", label: "Orbit Bike" },
      { value: "lunar-car", label: "Lunar Car" },
    ],
  },
  {
    value: "Systems",
    items: [
      { value: "telemetry", label: "Telemetry" },
      { value: "fuel-tank", label: "Fuel Tank" },
      { value: "ground-control", label: "Ground Control" },
    ],
  },
]

export default function Example() {
  return (
    <Autocomplete.Root items={groupedTags}>
      <label className="flex w-64 flex-col gap-2 text-sm font-medium">
        Pick a mission asset
        <Autocomplete.Input placeholder="E.g. rocket" />
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner>
          <Autocomplete.Popup>
            <Autocomplete.Empty>No mission assets found.</Autocomplete.Empty>
            <Autocomplete.List>
              {(group) => (
                <Autocomplete.Group
                  key={group.value}
                  items={group.items}
                  className="pb-1 last:pb-0"
                >
                  <Autocomplete.GroupLabel>{group.value}</Autocomplete.GroupLabel>
                  <Autocomplete.Collection>
                    {(tag) => (
                      <Autocomplete.Item key={tag.value} value={tag}>
                        {tag.label}
                      </Autocomplete.Item>
                    )}
                  </Autocomplete.Collection>
                </Autocomplete.Group>
              )}
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

Auto highlight

Set autoHighlight so the first match highlights as you type. Use "always" when the list stays visible, such as inside a dialog.

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

const tags = [
  { value: "rocket", label: "Rocket" },
  { value: "orbit-bike", label: "Orbit Bike" },
  { value: "lunar-car", label: "Lunar Car" },
]

export default function Example() {
  return (
    <Autocomplete.Root items={tags} autoHighlight>
      <label className="flex w-64 flex-col gap-2 text-sm font-medium">
        Auto highlight on type
        <Autocomplete.Input placeholder="E.g. rocket" />
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner>
          <Autocomplete.Popup>
            <Autocomplete.Empty>Nothing in the hangar matches.</Autocomplete.Empty>
            <Autocomplete.List>
              {(tag) => (
                <Autocomplete.Item key={tag.value} value={tag}>
                  {tag.label}
                </Autocomplete.Item>
              )}
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

Inline autocomplete

With mode="both", the list still filters as you type, and arrowing through options also fills the input with the highlighted label. Set mode to list to filter without filling, inline to fill without filtering, or none for neither.

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

const tags = [
  { value: "rocket", label: "Rocket" },
  { value: "orbit-bike", label: "Orbit Bike" },
  { value: "lunar-car", label: "Lunar Car" },
]

export default function Example() {
  return (
    // mode="both" filters the list and fills the input from the highlighted item.
    <Autocomplete.Root items={tags} mode="both">
      <label className="flex w-64 flex-col gap-2 text-sm font-medium">
        Search the hangar
        <Autocomplete.Input placeholder="E.g. rocket" />
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner className="data-empty:hidden">
          <Autocomplete.Popup>
            <Autocomplete.List>
              {(tag) => (
                <Autocomplete.Item key={tag.value} value={tag}>
                  {tag.label}
                </Autocomplete.Item>
              )}
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

Empty state and status

Keep Autocomplete.Empty and Autocomplete.Status mounted, and swap their children rather than the elements themselves so screen readers still announce changes.

import * as React from "react"
import { Autocomplete } from "@/components/ui/autocomplete"

const tags = [
  { value: "rocket", label: "Rocket" },
  { value: "orbit-bike", label: "Orbit Bike" },
  { value: "lunar-car", label: "Lunar Car" },
]

export default function Example() {
  const [value, setValue] = React.useState("")
  const { contains } = Autocomplete.useFilter({ sensitivity: "base" })

  const trimmed = value.trim()
  const matchCount = trimmed
    ? tags.filter((tag) => contains(tag.label, trimmed)).length
    : tags.length

  return (
    <Autocomplete.Root items={tags} value={value} onValueChange={setValue}>
      <label className="flex w-64 flex-col gap-2 text-sm font-medium">
        Search the hangar
        <Autocomplete.Input placeholder="E.g. rocket" />
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner>
          <Autocomplete.Popup>
            {/* Keep Empty/Status mounted; swap children so screen readers still announce. */}
            <Autocomplete.Status>
              {matchCount > 0
                ? `${matchCount} result${matchCount === 1 ? "" : "s"}`
                : null}
            </Autocomplete.Status>
            <Autocomplete.Empty>
              {trimmed ? `"${value}" is not cleared for launch.` : null}
            </Autocomplete.Empty>
            <Autocomplete.List>
              {(tag) => (
                <Autocomplete.Item key={tag.value} value={tag}>
                  {tag.label}
                </Autocomplete.Item>
              )}
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

Disabled

Pass disabled on Autocomplete.Root to disable the entire field. Pass disabled on an individual Autocomplete.Item to block that option while keeping the field active.

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

const tags = [
  { value: "rocket", label: "Rocket" },
  { value: "orbit-bike", label: "Orbit Bike" },
  { value: "classified", label: "Secret Payload", disabled: true },
  { value: "lunar-car", label: "Lunar Car" },
]

export default function Example() {
  return (
    <div className="flex w-full flex-col items-center gap-6">
      {/* Disable a single item by passing disabled to Autocomplete.Item */}
      <Autocomplete.Root items={tags}>
        <label className="flex w-64 flex-col gap-2 text-sm font-medium">
          Per-item disabled
          <Autocomplete.Input placeholder="E.g. rocket" />
        </label>
        <Autocomplete.Portal>
          <Autocomplete.Positioner>
            <Autocomplete.Popup>
              <Autocomplete.Empty>Nothing found.</Autocomplete.Empty>
              <Autocomplete.List>
                {(tag) => (
                  <Autocomplete.Item
                    key={tag.value}
                    value={tag}
                    disabled={tag.disabled}
                  >
                    {tag.label}
                  </Autocomplete.Item>
                )}
              </Autocomplete.List>
            </Autocomplete.Popup>
          </Autocomplete.Positioner>
        </Autocomplete.Portal>
      </Autocomplete.Root>

      {/* Disable the whole field with disabled on Root */}
      <Autocomplete.Root items={tags} disabled>
        <label className="flex w-64 flex-col gap-2 text-sm font-medium">
          Fully disabled
          <Autocomplete.Input placeholder="E.g. rocket" />
        </label>
        <Autocomplete.Portal>
          <Autocomplete.Positioner>
            <Autocomplete.Popup>
              <Autocomplete.List>
                {(tag) => (
                  <Autocomplete.Item key={tag.value} value={tag}>
                    {tag.label}
                  </Autocomplete.Item>
                )}
              </Autocomplete.List>
            </Autocomplete.Popup>
          </Autocomplete.Positioner>
        </Autocomplete.Portal>
      </Autocomplete.Root>
    </div>
  )
}

Async suggestions

For server-side search, pass filter={null} to disable built-in filtering and supply filteredItems from your debounced fetch. Use Autocomplete.Status to announce the loading state to screen readers while results are in flight.

import * as React from "react"
import { SearchIcon } from "lucide-react"
import { Autocomplete } from "@/components/ui/autocomplete"

const ALL_TAGS = [
  { value: "rocket", label: "Rocket" },
  { value: "orbit-bike", label: "Orbit Bike" },
  { value: "lunar-car", label: "Lunar Car" },
  { value: "telemetry", label: "Telemetry" },
  { value: "fuel-tank", label: "Fuel Tank" },
  { value: "ground-control", label: "Ground Control" },
]

export default function Example() {
  const [inputValue, setInputValue] = React.useState("")
  const [filteredItems, setFilteredItems] = React.useState(ALL_TAGS)
  const [loading, setLoading] = React.useState(false)
  const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined)

  function handleValueChange(value: string) {
    setInputValue(value)
    clearTimeout(timerRef.current)
    if (!value.trim()) {
      setFilteredItems(ALL_TAGS)
      setLoading(false)
      return
    }
    setLoading(true)
    // Simulate a remote fetch with a 400 ms debounce.
    timerRef.current = setTimeout(() => {
      const lower = value.toLowerCase()
      setFilteredItems(ALL_TAGS.filter((t) => t.label.toLowerCase().includes(lower)))
      setLoading(false)
    }, 400)
  }

  React.useEffect(() => () => clearTimeout(timerRef.current), [])

  return (
    <Autocomplete.Root
      // Pass filter={null} to disable built-in filtering; your filteredItems drive the list.
      filter={null}
      filteredItems={filteredItems}
      items={ALL_TAGS}
      value={inputValue}
      onValueChange={handleValueChange}
    >
      <label className="flex w-64 flex-col gap-2 text-sm font-medium">
        Search the hangar (async)
        <Autocomplete.InputGroup>
          <SearchIcon className="size-4 shrink-0 text-muted-foreground" aria-hidden />
          <Autocomplete.Input placeholder="E.g. rocket" />
          <Autocomplete.Clear />
        </Autocomplete.InputGroup>
      </label>
      <Autocomplete.Portal>
        <Autocomplete.Positioner>
          <Autocomplete.Popup>
            {/* Status is always mounted so screen readers announce changes. */}
            <Autocomplete.Status>
              {loading ? "Searching…" : null}
            </Autocomplete.Status>
            <Autocomplete.Empty>
              {!loading && inputValue.trim() ? "No results found." : null}
            </Autocomplete.Empty>
            <Autocomplete.List>
              {(tag) => (
                <Autocomplete.Item key={tag.value} value={tag}>
                  {tag.label}
                </Autocomplete.Item>
              )}
            </Autocomplete.List>
          </Autocomplete.Popup>
        </Autocomplete.Positioner>
      </Autocomplete.Portal>
    </Autocomplete.Root>
  )
}

Props

Autocomplete.Root

PropTypeDefaultDescription
itemsItemValue[] | { items: any[] }[]-Flat array of item values, or grouped array where each entry has an items array.
valuestring-Controlled text value of the input. Use with onValueChange.
defaultValuestring-Initial text value when uncontrolled.
onValueChange(value: string, eventDetails) => void-Callback fired when the input text changes. When items are objects, this receives the string produced by itemToStringValue.
openboolean-Controlled open state of the popup.
defaultOpenbooleanfalseWhether the popup starts open in uncontrolled mode.
onOpenChange(open: boolean, eventDetails) => void-Callback fired when the popup opens or closes.
mode'list' | 'both' | 'inline' | 'none''list'Controls filtering and inline completion. 'list' filters items. 'both' filters and previews the highlighted label in the input. 'inline' previews without filtering. 'none' disables both.
autoHighlightboolean | 'always'falseAutomatically highlights the first matching item. Use 'always' when the list remains permanently visible.
keepHighlightbooleanfalsePreserves the active highlight when the query text changes.
highlightItemOnHoverbooleantrueHighlights items on pointer hover. Set to false to limit highlight changes to keyboard navigation.
filter((itemValue, query, itemToString?) => boolean) | null-Custom filter function. Pass null to disable built-in filtering, which is required for async or server-side search.
filteredItemsany[] | Group[]-Externally filtered items array. Use alongside filter={null} when queries are resolved by an API.
itemToStringValue(itemValue: ItemValue) => string-Converts an item object to the text string displayed in the input. Not required if your objects use { value, label } structure.
limitnumber-1Maximum number of visible items. Set to -1 for unlimited.
gridbooleanfalseEnables two-dimensional arrow key navigation when items are arranged in rows and columns.
inlinebooleanfalseRenders the suggestions list directly in the document flow instead of a floating popup.
virtualizedbooleanfalseEnables virtual scrolling for large data sets.
openOnInputClickbooleanfalseOpens the suggestions menu when clicking the input, even before typing.
loopFocusbooleantrueCycles keyboard navigation from the last item back to the first, and vice versa.
modalbooleanfalseTraps focus inside the popup while open, similar to a dialog.
submitOnItemClickbooleanfalseSubmits the parent form immediately when an item is selected.
namestring-Form control name for the input.
disabledbooleanfalseDisables interaction with the autocomplete.
readOnlybooleanfalseMakes the field read-only. The suggestions popup still opens, but the text cannot be modified.
requiredbooleanfalseMarks the input as required for HTML5 form validation.

Autocomplete.Input

PropTypeDefault
disabledbooleanfalse
placeholderstring-

Autocomplete.Item

PropTypeDefault
valueanynull
disabledbooleanfalse
onClick(event) => void-
indexnumber-

Autocomplete.List

PropTypeDefault
childrenReactNode | ((item: any, index: number) => ReactNode)-

Autocomplete.Group

PropTypeDefault
itemsany[]-

Autocomplete.Positioner

PropTypeDefault
side'top' | 'bottom' | 'left' | 'right' | 'inline-end' | 'inline-start''bottom'
sideOffsetnumber | OffsetFunction4
align'start' | 'center' | 'end''center'
alignOffsetnumber | OffsetFunction0
collisionPaddingPadding5

Autocomplete.useFilter

PropTypeDefault
optionsAutocompleteFilterOptions-

Autocomplete.useFilteredItems

PropTypeDefaultDescription
(no parameters)returns T[]-Returns the current array of visible items after filtering. Helpful for computing match counts or display badges outside the list.

This covers the parts used above. See the Base UI Autocomplete docs for Backdrop, Arrow, Row, Value, event detail reasons, and every data attribute and CSS variable.