Components

Combobox

A form field that filters a list of options as you type.

Loading...

Installation

npx shadcn@latest add @fujin/combobox

Usage

import {
  Combobox,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxInput,
  ComboboxItem,
  ComboboxList,
} from "@/components/ui/combobox"
<Field name="framework">
  <FieldLabel>Framework</FieldLabel>
  <Combobox items={frameworks} value={value} onValueChange={setValue}>
    <ComboboxInput placeholder="Search frameworks..." showClear />
    <ComboboxContent>
      <ComboboxEmpty>No framework found.</ComboboxEmpty>
      <ComboboxList>
        {(framework) => (
          <ComboboxItem key={framework.value} value={framework}>
            {framework.label}
          </ComboboxItem>
        )}
      </ComboboxList>
    </ComboboxContent>
  </Combobox>
</Field>

Filtering runs against the items prop, not the rendered text: ComboboxList's child function is called for each match. Items shaped { value, label } need no configuration - the input shows label and the form submits value. For other shapes, pass itemToStringLabel and itemToStringValue.

Select, combobox or command?

  • select - a short list (roughly under 15) where scanning is faster than typing. No text input.
  • combobox - a longer list, in a form, where typing to narrow it helps. The value must be one of the options.
  • command - a search-to-pick list embedded in other UI (data-table's filter bar, a command palette), not a labelled form field.
  • Free text with suggestions (the typed text is the value) - use Base UI's Autocomplete; a combobox discards text that doesn't match an option.

What happens to typed text

Typed text is only a filter. In single mode, when the popup closes the input reverts to the selected item's label, or empties if nothing is selected. In multiple mode the filter text clears after each pick and on close.

Multiple selection with chips

Loading...

With no children, ComboboxChips renders a chip per selected item plus the input:

<Combobox items={labels} multiple defaultValue={[labels[0]]}>
  <ComboboxChips
    aria-label="Selected labels"
    placeholder="Add labels..."
    limit={3}
    showClear
  />
  <ComboboxContent>
    <ComboboxEmpty>No labels found.</ComboboxEmpty>
    <ComboboxList>
      {(item) => (
        <ComboboxItem key={item.value} value={item}>
          {item.label}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

From the start of the input, Left Arrow moves focus into the chips; Backspace or Delete removes the focused chip. The remove button is a 24px pointer target and is not in the tab order.

Handling many chips

  • The box stops growing. Chips wrap up to --combobox-chips-max-height (default 8.5rem, about four rows), then the box scrolls. Set the variable on ComboboxChips to change it.
  • limit collapses the selection while focus is elsewhere: only the first limit chips show, followed by "+N more". Clicking or tabbing into the field shows them all again, so every chip stays reachable by keyboard.
  • showClear adds a 24px "Clear all" button while anything is selected.

To render chips yourself (custom content, avatars), pass children. limit then no longer applies:

<ComboboxChips aria-label="Selected labels">
  <ComboboxValue>
    {(selected) => (
      <>
        {selected.map((item) => (
          <ComboboxChip key={item.value}>{item.label}</ComboboxChip>
        ))}
        <ComboboxChipsInput placeholder={selected.length ? "" : "Add..."} />
      </>
    )}
  </ComboboxValue>
</ComboboxChips>

For custom chip content with the built-in layout, use renderChip: <ComboboxChips renderChip={(user) => <><Avatar ... />{user.name}</>} />.

Creatable

Pass onCreate to let people add an option that isn't in the list. When the typed text matches no item's label exactly, a Create "<text>" option appears at the end of the list; picking it, or pressing Enter with nothing highlighted, calls onCreate(text).

Loading...
const [tags, setTags] = React.useState(initialTags)
 
<Combobox
  items={tags}
  multiple
  onCreate={async (label) => {
    const tag = await api.createTag(label)
    setTags((current) => [...current, tag])
    return tag
  }}
>
  <ComboboxChips aria-label="Selected tags" placeholder="Add tags..." />
  <ComboboxContent>
    <ComboboxEmpty>No tags found.</ComboboxEmpty>
    <ComboboxList>
      {(tag) => (
        <ComboboxItem key={tag.value} value={tag}>
          {tag.label}
        </ComboboxItem>
      )}
    </ComboboxList>
  </ComboboxContent>
</Combobox>
  • Return the new item to select it, or a promise of it. While the promise is pending, the option shows a spinner and can't be picked again. Return nothing to cancel.
  • Add it to items yourself. onCreate doesn't touch your list, so the item would vanish from it once deselected.
  • Handle errors inside onCreate. Show a toast or a field error; a rejected promise selects nothing.
  • Exact matches aren't duplicated. Text that matches an existing label (case-insensitive, trimmed) selects that item instead.
  • Your list renderer covers the create option. ComboboxItem renders the plus icon and label itself.
  • Flat items only. Grouped items get no create option.

It works the same in single mode. formatCreateLabel changes the option's text:

Loading...
<Combobox
  items={companies}
  value={company}
  onValueChange={setCompany}
  onCreate={(label) => {
    const created = { value: crypto.randomUUID(), label }
    setCompanies((current) => [...current, created])
    return created
  }}
  formatCreateLabel={(label) => `Add "${label}" as a new company`}
>
  <ComboboxInput placeholder="Search or add a company..." showClear />
  ...
</Combobox>

The logic lives in lib/combobox-creatable.ts (useComboboxCreatable), so you can use it on a bare Base UI Combobox too.

Groups

Pass groups as { value, items }[] and render each with a ComboboxCollection:

<ComboboxList>
  {(group) => (
    <ComboboxGroup key={group.value} items={group.items}>
      <ComboboxGroupLabel>{group.value}</ComboboxGroupLabel>
      <ComboboxCollection>
        {(item) => (
          <ComboboxItem key={item.value} value={item}>
            {item.label}
          </ComboboxItem>
        )}
      </ComboboxCollection>
    </ComboboxGroup>
  )}
</ComboboxList>

What's different from shadcn/ui

shadcn/ui builds a combobox by composing Popover and Command (cmdk). This one is Base UI's Combobox: one component with a real input as the form control, a hidden input for form submission, Field integration, and filtering driven by items.

API Reference

Prop

Type

Every other prop is forwarded to the Base UI Combobox.

On this page