Skip to documentation content

Combobox

Search and select one or more values from a predefined list.

combobox-demo

Overview

Combobox is a select whose list you can search. Type to filter, pick one value — or several, rendered as removable chips — from a list that can run into the hundreds.

Where it sits among the value controls:

  • Short closed list — Select or Radio Group; opening a filterable popup for six options adds steps, not clarity.
  • Long or growing list — Combobox. This component.
  • Input that may not exist on any list — Autocomplete (free text with suggestions) or Tags Input (collect many free values).
  • Running commands — Command. A combobox commits values; it never performs actions.

Anatomy

The field combines an input (which both filters and displays the selection), a popup of filtered options, and in multiple mode a chip per selected value with its own remove control. Optional parts cover everything real lists need: ComboboxGroup/ComboboxGroupLabel for structure, ComboboxEmpty for zero matches, ComboboxStatus for result counts, ComboboxClear for one-click reset, and ComboboxTrigger when the field should open like a select rather than type-first.

Behavior

Keyboard.

KeyResult
Character keysType to filter the options
ArrowDown / ArrowUpMove through matching options
EnterChoose the highlighted option
EscapeClose the popup (and clear the filter text first)
BackspaceIn multiple mode, removes the last chip

Filtering matches against option labels as you type. When nothing matches, ComboboxEmpty states that plainly — an empty popup with no message reads as breakage.

Multiple mode. Set multiple and every choice becomes a chip instead of replacing the input's text. Chips are individually removable without reopening the popup, and the input keeps filtering for the next pick. Selections survive further searching because they are held by reference, not by what the input currently shows.

Result status. With async data, ComboboxStatus reports how many options matched — the difference between "3 results" and a silently shorter list.

States

Disabled fields skip focus and submission. Invalid state follows aria-invalid with a danger border, announced to assistive technology; pair it with a FieldError explaining what to do. While options load asynchronously, keep the previous list visible and show loading via ComboboxStatus rather than blanking the popup — a flash of "no matches" during fetch teaches people their data is gone.

Long labels wrap inside the popup; chips truncate with ellipsis and expose their full label through aria-label. Colors come from tokens; layout mirrors in RTL because the popup anchors logically.

Accessibility

The input carries its accessible name from your visible label. The filtered list is a proper listbox: screen readers announce option count as you type, which option is highlighted, and each chip's name for removal ("Remove Riya Patel"). Nothing depends on color or position — selected options are marked by state, not styling alone.

Typeahead plus count announcements make large lists workable non-visually, but option copy still matters: put distinguishing words first ("Invoice INV-2042", not "INV-2042 — an invoice").

Installation

npx honestui@latest add combobox

Usage

import {
  Combobox,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
  ComboboxPopup,
} from "@/components/ui/combobox";
<Combobox items={teammates}>
  <ComboboxPopup>
    <ComboboxInput placeholder="Search teammates…" />
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item.value} value={item}>
          {item.label}
        </ComboboxItem>
      )}
    </ComboboxList>
    <ComboboxEmpty>No teammates match.</ComboboxEmpty>
  </ComboboxPopup>
</Combobox>

Don't do this

A closed set behind an open-texture control

// Bad
<Combobox items={[{ label: "Small" }, { label: "Medium" }, { label: "Large" }]} />
// Good
<Select items={sizes} />

When the whole list fits under a thumb, search is ceremony: people must type to see what they could have scanned instantly. Filtering pays off only when scanning costs more than typing — dozens of options, or ones people already know by name.

Silently empty while loading

// Bad
<ComboboxPopup>
  {loading ? null : <ComboboxList>…</ComboboxList>}
  {/* shows an empty popup mid-fetch */}
</ComboboxPopup>
// Good
<ComboboxPopup>
  <ComboboxStatus>{loading ? "Loading…" : `${matches.length} results`}</ComboboxStatus>
  <ComboboxList>…</ComboboxList>
</ComboboxPopup>

An unexplained blank list mid-fetch announces "no such person exists", and users conclude the feature is broken before the network answers. Say what is happening where the results would appear.

Examples

Multiple selection with chips

combobox-multiple

Grouped options

combobox-grouped

Clearable

combobox-with-clear

In a form

combobox-form

API reference

PropValuesDefault
items{ label: string; value: unknown }[]—
multiplebooleanfalse
value / defaultValueitem or item[]—

Parts: ComboboxInput, ComboboxTrigger, ComboboxPopup, ComboboxList, ComboboxItem, ComboboxChip(s), ComboboxGroup(Label), ComboboxEmpty, ComboboxStatus, ComboboxClear, ComboboxValue.

See the Base UI Combobox API.