Pho Design System

Combobox

A text input that filters a list as you type and commits to a value from it. Built on Base UI Combobox. For free-form text with suggestions use Autocomplete; for a short fixed set use Select.

Import

import { Combobox } from "@photon-ai/pho-ui/components/combobox";

Basic

Pass items to Combobox.Root. Combobox.List takes a render function that maps each filtered item to a Combobox.Item; Combobox.Empty shows when nothing matches. When items are { value, label } objects, the popup filters on label and the input shows it without any extra wiring.

import { IconCheck, IconSelector } from "@tabler/icons-react";

<Combobox.Root items={fruits}>
  <Combobox.InputGroup>
    <Combobox.Input placeholder="Search fruits…" />
    <Combobox.Trigger aria-label="Open">
      <IconSelector />
    </Combobox.Trigger>
  </Combobox.InputGroup>
  <Combobox.Portal>
    <Combobox.Positioner>
      <Combobox.Popup>
        <Combobox.Empty>No fruits found.</Combobox.Empty>
        <Combobox.List>
          {(item) => (
            <Combobox.Item key={item.value} value={item}>
              <Combobox.ItemIndicator>
                <IconCheck />
              </Combobox.ItemIndicator>
              {item.label}
            </Combobox.Item>
          )}
        </Combobox.List>
      </Combobox.Popup>
    </Combobox.Positioner>
  </Combobox.Portal>
</Combobox.Root>;

Multi-select

Set multiple on the root and swap Combobox.InputGroup for Combobox.Chips. Render Combobox.Value with a function that maps each selected value to a Combobox.Chip (with a Combobox.ChipRemove), then place the Combobox.Input after it so typing continues inline. The value is now an array; Backspace on an empty input removes the last chip.

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

<Combobox.Root items={fruits} multiple>
  <Combobox.Chips>
    <Combobox.Value>
      {(selected) =>
        selected.map((item) => (
          <Combobox.Chip key={item.value} aria-label={item.label}>
            {item.label}
            <Combobox.ChipRemove aria-label={`Remove ${item.label}`}>
              <IconX />
            </Combobox.ChipRemove>
          </Combobox.Chip>
        ))
      }
    </Combobox.Value>
    <Combobox.Input placeholder="Add fruits…" />
  </Combobox.Chips>
  <Combobox.Portal>{/* …same popup as Basic… */}</Combobox.Portal>
</Combobox.Root>;

Grouped options

Pass an array of { items } groups to render labelled sections. The Combobox.List render function then receives each group instead of each item: wrap the group’s items in a Combobox.Group, label it with Combobox.GroupLabel, and map them to Combobox.Items through a Combobox.Collection. The built-in filter still matches across every group at once.

import { IconCheck, IconSelector } from "@tabler/icons-react";

<Combobox.Root items={produce}>
  {/* …same input group as Basic… */}
  <Combobox.Portal>
    <Combobox.Positioner>
      <Combobox.Popup>
        <Combobox.Empty>No produce found.</Combobox.Empty>
        <Combobox.List>
          {(group) => (
            <Combobox.Group key={group.value} items={group.items}>
              <Combobox.GroupLabel>{group.value}</Combobox.GroupLabel>
              <Combobox.Collection>
                {(item) => (
                  <Combobox.Item key={item.value} value={item}>
                    <Combobox.ItemIndicator>
                      <IconCheck />
                    </Combobox.ItemIndicator>
                    {item.label}
                  </Combobox.Item>
                )}
              </Combobox.Collection>
            </Combobox.Group>
          )}
        </Combobox.List>
      </Combobox.Popup>
    </Combobox.Positioner>
  </Combobox.Portal>
</Combobox.Root>;

Controlled

Everything is uncontrolled by default. To drive the committed value from your own state, pass value + onValueChange; for the initial value only, use defaultValue. The typed text is separate state — control it with inputValue + onInputValueChange (or defaultInputValue), and control open/closed with open

const [value, setValue] = useState<Fruit | null>(null);

<Combobox.Root items={fruits} value={value} onValueChange={setValue}>
  {/* … */}
</Combobox.Root>;

Anatomy

Props

Props go on Combobox.Root unless noted; every part also forwards its Base UI props plus className.

Accessibility

Keyboard:

Best practices