Skip to content

Combobox

Filters a list of options as you type, for one value or several shown as pills.

Library
What’s this?

Installation

pnpm dlx shadcn@latest add @wisty/combobox

The CLI installs the Radix or Base UI file to match your project, with what it builds on: button, input-group.

If your project doesn't have Wisty UI's theme yet, add it once with npx shadcn@latest add @wisty/cosmos. Until then, components keep shadcn's default look. Installation has the details.

When to use

Use it

  • People pick a value from a long list and typing to filter it helps, like a country, a product or an industry.
  • People pick several values from such a list. The chips parts show each picked value as a pill in the field.
  • The options are in the page, or you fetch them yourself as people type.

Don't use it

  • The list is short and people don't need to type. Use Select.
  • The value is any text, with no list to pick from. Use Input.
  • People pick Salesforce records, with object icons, a meta line, recent records or a search as they type. Use Lookup, which is built on this combobox.
  • The list holds actions, like Edit or Delete, rather than values. Use Dropdown menu.
  • There are only a few choices and people should see them all. Use Radio group or Checkbox.

Usage

Pass the options to Combobox as items, and render one ComboboxItem per option from a function inside ComboboxList. A function as children needs a client component, so mark the file "use client".

Combobox
├── ComboboxInput
└── ComboboxContent
    ├── ComboboxEmpty
    └── ComboboxList
        └── ComboboxItem
"use client"

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
<Combobox items={["Agriculture", "Banking", "Education", "Retail"]}>
  <ComboboxInput aria-label="Industry" placeholder="Search industries..." />
  <ComboboxContent>
    <ComboboxEmpty>No industries found.</ComboboxEmpty>
    <ComboboxList>
      {(industry: string) => (
        <ComboboxItem key={industry} value={industry}>
          {industry}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

ComboboxInput has a chevron button that opens the list; showTrigger={false} removes it. showClear adds a clear button, which takes the chevron's place while the field has a value.

For several values, pass multiple to Combobox and build the field from chips. Call useComboboxAnchor() and give its ref to ComboboxChips as ref and to ComboboxContent as anchor, so the list opens below the whole field, as the Multiple example shows. Each ComboboxChip has a remove button unless you pass showRemove={false}.

Combobox
├── ComboboxChips
│   └── ComboboxValue
│       ├── ComboboxChip
│       └── ComboboxChipsInput
└── ComboboxContent
    ├── ComboboxEmpty
    └── ComboboxList
        └── ComboboxItem

For groups, pass items as groups that each hold their own items. The function inside ComboboxList then returns a ComboboxGroup per group, given that group's items, and a function inside its ComboboxCollection returns the options, as the Groups example shows.

ComboboxList
└── ComboboxGroup
    ├── ComboboxSeparator
    ├── ComboboxLabel
    └── ComboboxCollection
        └── ComboboxItem

onValueChange gets the new value, or null when the field is cleared. For object items, give Combobox an itemToStringLabel so the field shows a label.

In Radix projects, Combobox is built on @base-ui/react, as shadcn's is, so installing it adds that package beside radix-ui. The two work side by side.

Examples

Groups

Option headers from `ComboboxLabel`, a `ComboboxSeparator`, a disabled option, and a selected value with the `showClear` button.

Multiple

Several values as pills inside the field, set with `multiple` on `Combobox` and a `ComboboxChip` per value.

States

An industry marked with `aria-invalid` above its `FieldError`, and a region with a value and `disabled`.

In a dialog

A loss reason in a modal dialog, where the list opens above the dialog and Escape closes the list before the dialog.

API reference

PartBuilt on
ComboboxBase UI Combobox.Root (opens in a new tab)
ComboboxValueBase UI Combobox.Value (opens in a new tab)
ComboboxTriggerBase UI Combobox.Trigger (opens in a new tab)
ComboboxInputBase UI Combobox.Input (opens in a new tab)
ComboboxContentBase UI Combobox.Popup (opens in a new tab)
ComboboxListBase UI Combobox.List (opens in a new tab)
ComboboxItemBase UI Combobox.Item (opens in a new tab)
ComboboxGroupBase UI Combobox.Group (opens in a new tab)
ComboboxLabelBase UI Combobox.GroupLabel (opens in a new tab)
ComboboxCollectionBase UI Combobox.Collection (opens in a new tab)
ComboboxEmptyBase UI Combobox.Empty (opens in a new tab)
ComboboxSeparatorBase UI Combobox.Separator (opens in a new tab)
ComboboxChipsBase UI Combobox.Chips (opens in a new tab)
ComboboxChipBase UI Combobox.Chip (opens in a new tab)
ComboboxChipsInputBase UI Combobox.Input (opens in a new tab)

ComboboxInput

PropTypeDefaultDescription
showTriggerbooleantrue
showClearbooleanfalse
disabledbooleanfalseWhether the component should ignore user interaction.

ComboboxContent

PropTypeDefaultDescription
align"center" | "end" | "start""start"How to align the popup relative to the specified side.
side"top" | "bottom" | "left" | "right" | "inline-end" | "inline-start""bottom"Which side of the anchor element to align the popup against.
sideOffsetnumber | OffsetFunction2Distance between the anchor and the popup in pixels.
alignOffsetnumber | OffsetFunction0Additional offset along the alignment axis in pixels.
anchorElement | VirtualElement | RefObject<Element | null> | (() => Element | VirtualElement | null) | nullAn element to position the popup against.

ComboboxChip

PropTypeDefault
showRemovebooleantrue

useComboboxAnchorhook

Returns RefObject<HTMLDivElement | null>.

SLDS mapping

Combobox draws SLDS's autocomplete combobox. It runs on Base UI's combobox in Radix projects too, so installing it in a Radix project adds @base-ui/react.

  • ComboboxInput is slds-combobox__input in slds-input-has-icon_right, with the chevron as the input icon. Its clear button is SLDS's input icon button.
  • ComboboxContent is slds-dropdown slds-dropdown_fluid slds-dropdown_length-with-icon-7, as Select's list.
  • ComboboxItem is slds-listbox__option slds-listbox__option_plain, with SLDS's check column on the left.
  • ComboboxLabel is slds-listbox__option-header, and ComboboxSeparator is slds-has-divider_top-space. ComboboxEmpty is a plain option row in muted text, since SLDS has no "no results" row.
  • ComboboxChips is SLDS's inline listbox of pills (slds-has-inline-listbox, slds-listbox_inline), and ComboboxChip is a slds-pill option.

Known gaps:

  • SLDS's autocomplete shows a search icon. Here the field has a chevron, which SLDS 2's combobox anatomy also names.
  • SLDS 2 shows several values as pills below the field. Here they sit inside it, as in SLDS's inline listbox.
  • Option icons, metadata lines, a loading spinner and the 5 and 10 row list heights aren't drawn. For records with icons and meta lines, use Lookup.
PropValueSLDSNotes
ComboboxInputdefaultslds-combobox_container, slds-combobox__input, slds-input-has-icon_rightAn input with the chevron button inside, 32px.
ComboboxInput showCleartrueslds-input__icon_right (clear button)A 24px icon button replaces the chevron while there's a value.
ComboboxContentdefaultslds-dropdown slds-dropdown_fluid, slds-listbox_verticalOpens 2px below the field, as wide as it, seven rows tall.
ComboboxChipsdefaultslds-has-inline-listbox, slds-listbox_inlineSelected values as pills inside the field.
ComboboxChipdefaultslds-pill (a pill option)Looks like a pill in a PillGroup.

Keyboard interactions

Keys Action
Typing Filters the options and opens the list.
Down Arrow, Up Arrow Opens the list, or moves the highlight. Opening highlights the selected option, if any.
Enter Selects the highlighted option and closes the list.
Escape Closes the list. On a closed field with a value, clears it.
Left Arrow (chips) From the start of the input, moves to the last chip, then along the chips. Right Arrow returns.
Backspace (chips) In an empty input, removes the last chip. On a focused chip, removes it.

Inside a Dialog, Sheet, Alert dialog or Popover, the first Escape closes only the list and the next closes the dialog. On a closed field with a value, Escape clears it; in Radix projects the same Escape also closes the dialog.

Accessibility

  • Name the input: a FieldLabel whose htmlFor matches the input's id, or aria-label.
  • For an error, set aria-invalid on ComboboxInput and point aria-describedby at the message, as the States example does.
  • The chevron, clear and chip remove buttons are out of the Tab order and named "Show options", "Clear" and "Remove". No prop changes those names, so for another language, edit them in your copy of the file.
  • While the list is open, everything outside the field and the list is hidden from screen readers, the label and error included. The input keeps its name from the label.
  • ComboboxEmpty is a status, so screen readers announce it when nothing matches. Always include one.
  • In Cosmos dark, the invalid field's text reaches about 4.3:1, below WCAG AA's 4.5:1. Salesforce's own stylesheet fails the same way.

Responsive behavior

  • The field fills its container. The list is as wide as the field, shows seven rows, then scrolls, and opens above the field when there's no room below.
  • Chips wrap onto more rows as the field fills, and the input keeps at least 4rem.
  • Long option labels wrap in the list. A long value in the field scrolls with the text.

Performance

  • Filtering runs over items on every keystroke. A few hundred items are fine.
  • For thousands, or for records on a server, filter yourself: pass filteredItems or control inputValue, and fetch as people type.

Rules

  • Disable with disabled on both Combobox and ComboboxInput, as the States example does.
  • Don't add your own Escape or outside-click handling inside a dialog. The dialogs already leave Escape to an open list.