Skip to content

Pickers and overlays

Combobox

A text box that can also offer matching choices.

When to use it: Use it when people may type to narrow a long set of choices.

On this page

Example

Combobox.tsxtsx
import {
  Combobox,
  ComboboxInput,
  ComboboxOptGroup,
  ComboboxOption,
  ComboboxPopover,
  ComboboxTrigger,
  Label,
} from "@comp0/react";
import { ChevronDownIcon } from "@heroicons/react/16/solid";

export function Example() {
  return (
    <Combobox as="div" className="flex max-w-xs flex-col gap-1.5" name="city">
      <Label className="text-base font-medium text-zinc-900 sm:text-sm dark:text-zinc-100">
        City
      </Label>
      <div className="flex">
        <ComboboxInput
          className="w-full rounded-l border border-r-0 border-zinc-950/10 bg-white px-3 py-2.5 text-base text-zinc-950 outline-teal-600 focus-visible:z-10 focus-visible:outline-2 sm:py-2 sm:text-sm dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-50 dark:outline-teal-400"
          placeholder="Filter cities"
        />
        <ComboboxTrigger className="group rounded-r border border-zinc-950/10 bg-white px-3 text-zinc-500 outline-teal-600 focus-visible:z-10 focus-visible:outline-2 dark:border-white/10 dark:bg-zinc-900 dark:text-zinc-400 dark:outline-teal-400">
          <ChevronDownIcon
            className="size-4 transition-transform duration-150 ease-out group-data-open:rotate-180 motion-reduce:transition-none"
            aria-hidden="true"
          />
        </ComboboxTrigger>
      </div>
      <ComboboxPopover
        placement="bottom"
        offset={4}
        className="max-h-60 w-[anchor-size(width)] max-w-[anchor-size(width)] overflow-y-auto rounded border-0 bg-white p-1 opacity-100 ring-1 ring-zinc-950/10 transition-[opacity,translate] duration-150 ease-out starting:-translate-y-1 starting:opacity-0 motion-reduce:transition-none dark:bg-zinc-900 dark:ring-white/10"
      >
        <ComboboxOptGroup label="Europe">
          <div
            aria-hidden="true"
            className="px-3 pb-1 pt-2 text-xs font-medium text-zinc-500 dark:text-zinc-400"
          >
            Europe
          </div>
          <ComboboxOption
            className="cursor-pointer rounded px-3 py-2.5 text-base text-zinc-800 hover:bg-zinc-100 data-active:bg-teal-100 data-active:text-zinc-950 data-selected:bg-teal-100 focus-visible:bg-teal-200 focus-visible:outline-2 focus-visible:outline-teal-600 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800 dark:data-active:bg-teal-950 dark:data-active:text-zinc-50 dark:data-selected:bg-teal-950 dark:focus-visible:bg-teal-800 dark:focus-visible:outline-teal-400"
            value="Warsaw"
          >
            Warsaw
          </ComboboxOption>
          <ComboboxOption
            className="cursor-pointer rounded px-3 py-2.5 text-base text-zinc-800 hover:bg-zinc-100 data-active:bg-teal-100 data-active:text-zinc-950 data-selected:bg-teal-100 focus-visible:bg-teal-200 focus-visible:outline-2 focus-visible:outline-teal-600 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800 dark:data-active:bg-teal-950 dark:data-active:text-zinc-50 dark:data-selected:bg-teal-950 dark:focus-visible:bg-teal-800 dark:focus-visible:outline-teal-400"
            value="Lisbon"
          >
            Lisbon
          </ComboboxOption>
        </ComboboxOptGroup>
        <ComboboxOptGroup label="Asia">
          <div
            aria-hidden="true"
            className="px-3 pb-1 pt-2 text-xs font-medium text-zinc-500 dark:text-zinc-400"
          >
            Asia
          </div>
          <ComboboxOption
            className="cursor-pointer rounded px-3 py-2.5 text-base text-zinc-800 hover:bg-zinc-100 data-active:bg-teal-100 data-active:text-zinc-950 data-selected:bg-teal-100 focus-visible:bg-teal-200 focus-visible:outline-2 focus-visible:outline-teal-600 sm:py-2 sm:text-sm dark:text-zinc-100 dark:hover:bg-zinc-800 dark:data-active:bg-teal-950 dark:data-active:text-zinc-50 dark:data-selected:bg-teal-950 dark:focus-visible:bg-teal-800 dark:focus-visible:outline-teal-400"
            value="Tokyo"
          >
            Tokyo
          </ComboboxOption>
        </ComboboxOptGroup>
      </ComboboxPopover>
    </Combobox>
  );
}

Anatomy

Dashed frames are invisible state providers; shaded shapes own real DOM. Numbered pins match the list below.

A wireframe sketch of the assembled component. Each numbered marker matches a part in the list that follows.

  1. Combobox

    Selected-value, open-state, and form-serialization provider. Does not add a DOM element.

  2. Label

    Visible name connected to the input. Owns a DOM element.

  3. ComboboxInput

    Native text input that owns editing and required validity. Owns a DOM element.

  4. ComboboxTriggerOptional

    Optional button that opens or closes the suggestions with a pointer. Owns a DOM element.

  5. ComboboxPopover

    The listbox results surface. Owns a DOM element.

  6. ComboboxOptGroupOptional

    Optional native-style group of related results. Owns a DOM element.

  7. ComboboxOption

    Selectable result. Owns a DOM element.

Step by step

  1. 1

    Add the main part

    Start Combobox with a labelled ComboboxInput.

  2. 2

    Add the supporting parts

    Place ComboboxTrigger beside the input, then add ComboboxPopover as the listbox of results; group related results with ComboboxOptGroup.

  3. 3

    Make the behavior clear

    Combobox owns the selected value, field, form serialization, and open state; control the results with open, defaultOpen, and onToggle.

    Exampletsx
    <Combobox name="city"><Label>City</Label><ComboboxInput /><ComboboxTrigger aria-label="Show suggestions" /><ComboboxPopover><ComboboxOption value="Paris">Paris</ComboboxOption></ComboboxPopover></Combobox>

Keyboard

Moves through options.
Selects the active option from the input or opens from ComboboxTrigger.
Space
Opens suggestions from ComboboxTrigger.
Esc
Closes results.

Forms and accessibility

Combobox.name owns selected-value serialization. ComboboxInput owns text editing and required validity.

Accessibility checklist

  • Use a visible Label so the input and results list share a clear name; without one, name both parts explicitly.
  • Give every ComboboxOptGroup a native label so grouped results have a name.
  • Keep option text specific enough to distinguish matches.
  • Do not hide required instructions only in the result list.

API reference

Importtsx
import { Combobox, ComboboxInput, ComboboxOptGroup, ComboboxOption, ComboboxPopover, ComboboxTrigger, Label } from "@comp0/react";

Combobox

Context only

Selected-value, open-state, and form-serialization provider.

PropTypeDescription
valuestringControlled committed option.
defaultValuestringInitial committed option.
onChange(value: string) => voidReceives the committed option.
inputValue / defaultInputValuestringControlled or initial editable text.
onInputChange(value: string) => voidReceives the editable text.
open / defaultOpenbooleanControlled or initial open state of the results.
onToggle(open: boolean) => voidReceives the next open state.
filter(textValue, inputValue) => booleanCustom match rule for results.
namestringSubmission name.
formstringAssociates the combobox value with a form by id.

Label

DOM element

Visible name connected to the input.

ComboboxInput

DOM element

Native text input that owns editing and required validity.

PropTypeDescription
placeholderstringHint text; never a replacement for Label.
aria-labelstringNames the input when there is no visible Label.
disabled / requiredbooleanOverride the field-wide state for this control.

ComboboxTrigger

OptionalDOM element

Optional button that opens or closes the suggestions with a pointer.

PropTypeDescription
aria-labelstringNames the button; defaults to the English "Show suggestions".
disabledbooleanDisables the trigger and inherits Combobox.disabled.

ComboboxPopover

DOM element

The listbox results surface.

PropTypeDescription
aria-labelstringNames the results list when there is no visible Label.
placementPopoverPlacementInput side to open on, such as "bottom"; flips when there is no room.
offsetnumberPixel gap between the input and the listbox.

ComboboxOptGroup

OptionalDOM element

Optional native-style group of related results.

PropTypeDescription
labelstringNames the group before its results.

ComboboxOption

DOM element

Selectable result.

PropTypeDescription
valuestringThis result’s value.
disabledbooleanDisables the result.
textValuestringOverrides the text crawled from children when markup makes it ambiguous.

Style hooks

Attributes that appear while a state is true. Target them with Tailwind data variants such as data-open:bg-zinc-100, or with any CSS selector.

[data-open]

on ComboboxInput, ComboboxTrigger, ComboboxPopover

Results are open.
:popover-open

on ComboboxPopover

Native pseudo-class equivalent.
[data-selected]

on ComboboxOption

Option is selected.
[data-active]

on ComboboxOption

Option is active and receives the visible keyboard highlight.
[aria-activedescendant]

on ComboboxInput

The input points to the keyboard-active option while focus stays in the input.

Keep exploring