Pho Design System

Autocomplete

A free-text input that offers matching suggestions as you type. Built on Base UI Autocomplete. Unlike Combobox, the value isn’t constrained to the list — the user can submit text that matches no suggestion.

Import

import { Autocomplete } from "@photon-ai/pho-ui/components/autocomplete";

Basic

Pass items to Autocomplete.Root; Autocomplete.List maps each suggestion to an Autocomplete.Item. The list filters as you type, and the typed text is the value whether or not it matches — Autocomplete.Empty renders when nothing does.

<Autocomplete.Root items={labels}>
  <Autocomplete.InputGroup>
    <Autocomplete.Input placeholder="Add a label…" />
  </Autocomplete.InputGroup>
  <Autocomplete.Portal>
    <Autocomplete.Positioner>
      <Autocomplete.Popup>
        <Autocomplete.Empty>
          No matches — press Enter to add.
        </Autocomplete.Empty>
        <Autocomplete.List>
          {(label) => (
            <Autocomplete.Item key={label} value={label}>
              {label}
            </Autocomplete.Item>
          )}
        </Autocomplete.List>
      </Autocomplete.Popup>
    </Autocomplete.Positioner>
  </Autocomplete.Portal>
</Autocomplete.Root>

Clear

Add Autocomplete.Clear inside the InputGroup for a one-click reset. It’s an icon-only button, so it needs an aria-label; it mounts only while the field has a value and unmounts when empty. Pad the input’s right edge so text clears the button.

import { IconX } from "@tabler/icons-react";

<Autocomplete.InputGroup>
  <Autocomplete.Input className="pr-9" placeholder="Add a label…" />
  <Autocomplete.Clear aria-label="Clear">
    <IconX />
  </Autocomplete.Clear>
</Autocomplete.InputGroup>;

Grouped options

Pass an array of { items } groups instead of a flat list to bucket suggestions under headings. Each List child is a group — render an Autocomplete.Group with its Autocomplete.GroupLabel, then an Autocomplete.Collection that maps the group’s own items.

const groups = [
  { value: "Environments", items: ["production", "staging", "preview"] },
  { value: "Regions", items: ["us-east", "us-west", "eu-central"] },
];

<Autocomplete.Root items={groups}>
  {/* …InputGroup / Portal / Positioner / Popup… */}
  <Autocomplete.List>
    {(group) => (
      <Autocomplete.Group key={group.value} items={group.items}>
        <Autocomplete.GroupLabel>{group.value}</Autocomplete.GroupLabel>
        <Autocomplete.Collection>
          {(item) => (
            <Autocomplete.Item key={item} value={item}>
              {item}
            </Autocomplete.Item>
          )}
        </Autocomplete.Collection>
      </Autocomplete.Group>
    )}
  </Autocomplete.List>
</Autocomplete.Root>;

Controlled

The value is the input’s text — control it with value + onValueChange to read what the user typed (matching suggestion or not), sync it to a URL query, or seed it from elsewhere. Use defaultValue for the initial text only.

Empty — start typing
const [value, setValue] = useState("");

<Autocomplete.Root items={labels} value={value} onValueChange={setValue}>
  {/* …InputGroup / Portal / Positioner / Popup… */}
</Autocomplete.Root>;

Anatomy

Props

Set on Autocomplete.Root; the wrapper forwards the rest of Base UI’s Autocomplete.Root props.

Parts (Input, Clear, Popup, Item, …) each take a className and their native element attributes.

Accessibility

Keyboard:

Best practices