Photon 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

  • Autocomplete.Root — owns items, the free-text value, and open state
  • Autocomplete.InputGroup — wraps the control; positions Input and Clear
  • Autocomplete.Input — the editable combobox field
  • Autocomplete.Clear — icon button that resets the value (mounts when non-empty)
  • Autocomplete.Portal → Positioner → Popup — the floating suggestion layer
  • Autocomplete.List — the listbox; maps items (or groups) to children
  • Autocomplete.Item — one selectable option
  • Autocomplete.Empty — the no-match message (a polite live region)
  • Autocomplete.Group / GroupLabel / Collection — heading + items for grouped data

Props

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

  • items — readonly T[] or readonly { items: T[] }[] — the suggestion data, flat or grouped
  • mode — list · both · inline · none (default list) — filtering + inline-completion behavior
  • value / defaultValue — string — the input’s text value, controlled / uncontrolled
  • onValueChange — (value: string, details) => void — fired as the text changes
  • filter — ((itemValue, query) => boolean) | null — custom matcher, or null to disable filtering
  • autoHighlight — boolean | "always" (default false) — highlight the first match automatically
  • openOnInputClick — boolean (default false) — open the popup when the input is clicked
  • open / defaultOpen — boolean — control popup visibility; onOpenChange — (open, details) => void
  • disabled / readOnly / required — boolean (default false)
  • name — string — field name for form submission

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

Accessibility

  • Semantics — Input is a role="combobox"; List is its role="listbox" and each Item a role="option". Base UI wires aria-expanded, aria-controls, and aria-activedescendant between them. Give the input a real accessible name with a <label> or aria-label — a placeholder alone doesn’t name the field. Autocomplete.Clear is icon-only and needs its own aria-label.
  • Live region — Autocomplete.Empty renders role="status" with aria-live="polite", so its no-match text is announced when suggestions run out. Keep that message useful (No matches — press Enter to add).
  • Focus — virtual: DOM focus stays on the input while the active option is tracked via aria-activedescendant, so typing never leaves the field.

Keyboard:

  • Type — filter the list
  • Down / Up — move the highlight through options (virtual focus)
  • Enter — accept the highlighted suggestion; with nothing highlighted, your typed text stands
  • Escape — close the popup
  • Home / End — move the text cursor to the start / end of the input

Best practices

  • Use autocomplete when arbitrary text is valid but suggestions speed things up — labels, tags, search queries. If the value must come from the list, use Combobox.
  • Make Autocomplete.Empty actionable (“press Enter to add”) so a no-match state still moves the user forward.
  • Keep suggestions ranked by relevance; with autoHighlight on, the first item is what Enter selects, so rank matters.
  • Label the input properly — a placeholder is a hint, not an accessible name.