Combobox

Search predefined options and select one or several values with keyboard support.

Preview

Use Combobox when the list is long enough to benefit from typing to filter. Use Select or Native Select for short lists. This component selects predefined values; use an autocomplete pattern for free text or creating new values.

Type to filter, then select a result. Ember is unavailable in this demo.

Installation

pnpm dlx shadcn@latest add http://127.0.0.1:3017/r/combobox.json

Installs Combobox, Base UI, Lucide, and Migoy Input Group with its component dependencies. Review existing customized files before adopting updates. Your palette and CSS are preserved; Field and the Migoy theme remain optional separate items. This URL is a local development snapshot.

Current shadcn uses Base UI Combobox even in its Radix collection. Migoy follows that implementation for filtering, selection, keyboard navigation, and announcements while reusing its existing Input Group and Button. Existing Migoy Select remains Radix-based; their APIs are separate.

Usage

import {
  Combobox, ComboboxInput, ComboboxContent,
  ComboboxEmpty, ComboboxList, ComboboxItem,
} from '@/components/ui/combobox';

Combobox is a client component. Pass the full items collection on the root, then render its filtered items through ComboboxList's child function. Put labels on ComboboxInput using its id and a visible FieldLabel, or use aria-label when a visible label cannot be rendered. className on ComboboxInput applies layout classes to its surrounding Input Group.

value / defaultValue and onValueChange refer to the selected item. inputValue and onInputValueChange refer to search text. They are different states. For single selection, the cleared value is null; for multiple selection, use an array. showClear adds a named clear button; the trigger is named Show options.

For objects, pass each item object as ComboboxItem's value and configure the root's itemToStringLabel for displayed/filterable text and itemToStringValue for form serialization. Use stable IDs when your product stores IDs rather than labels. See the linked Base UI reference for typed collections and asynchronous filtering.

Composition

For a searchable single-value control, keep the input and popup inside the same root:

Combobox
├── ComboboxInput
└── ComboboxContent
    ├── ComboboxEmpty
    └── ComboboxList
        └── ComboboxItem (rendered through the list's child function)

Combobox owns the item collection, filtering, and selection. ComboboxInput includes its Input Group and optional trigger and clear actions. ComboboxContent handles its portal and positioning. Keep the empty state mounted and let ComboboxList provide each filtered item to its render function.

For multiple selection with inline search, replace ComboboxInput with ComboboxChips containing ComboboxValue, ComboboxChip, and ComboboxChipsInput, then anchor ComboboxContent to the chips container. The multiple-selection examples show this structure and the separate MultiSelect wrapper. Grouped collections use ComboboxGroup, ComboboxLabel, and ComboboxCollection as described under Groups and accessibility.

The trigger chevron rotates 180° while the popup is open, with a 150ms transition. Reduced-motion preferences disable the transition.

Sizes

Type to filter, then select a result. Ember is unavailable in this demo.

Type to filter, then select a result. Ember is unavailable in this demo.

Type to filter, then select a result. Ember is unavailable in this demo.

controlSize on ComboboxInputHeightRadiusDesktop input text
sm24px3px12px / 16px
default32px4px14px / 20px
lg44px6px14px / 20px
<ComboboxInput controlSize="sm" aria-label="Framework" placeholder="Search frameworks…" />

Input Group owns the shared border, hover, disabled opacity, and solid offset focus outline. Editing text stays 16px on smaller screens, matching Migoy Input. The popup matches its anchor width, has a 6px radius, uses the semantic popover surface, and stays within available viewport space. Long lists scroll. Chips use a flexible default-size container that grows as values wrap.

Disabled and invalid

Type to filter, then select a result. Ember is unavailable in this demo.

Type to filter, then select a result. Ember is unavailable in this demo.

Put disabled on Combobox to disable the whole widget, or on a ComboboxItem to make one result unavailable. In this page's first example, Ember remains visible but cannot be selected. Base UI allows keyboard highlighting of disabled results so their presence can be discovered; Enter does not select them. For validation, put data-invalid on Field and aria-invalid on ComboboxInput; link FieldError with aria-describedby. The input's invalid state is reflected in the shared Input Group boundary.

Multiple selection

  • Next.js
1 selected

Click the field to search. Remove a chip to deselect it.

For many selections, use the separate MultiSelect composition. The closed field displays chips; clicking its non-interactive area or trigger opens a full-width search in the popup. Search receives focus on every open, including touch. Selecting keeps the popup open and retains the filter. Closing clears the filter; Escape returns focus to the trigger. Removing a chip does not open the popup and focuses a neighboring remove button, or the trigger when the last chip is removed.

pnpm dlx shadcn@latest add http://127.0.0.1:3017/r/multi-select.json
import { MultiSelect } from '@/components/ui/multi-select';

<MultiSelect
  aria-label="Supported frameworks"
  name="frameworks"
  options={[
    { value: 'next', label: 'Next.js' },
    { value: 'astro', label: 'Astro' },
    { value: 'nuxt', label: 'Nuxt', disabled: true },
  ]}
  defaultValue={['next']}
/>

Use unique string value IDs with display labels. Use value / onValueChange for controlled selection, or defaultValue for uncontrolled selection; do not switch modes after mount. Options can be disabled individually. Values not currently present in options are retained and displayed by their ID, rather than silently dropped; provide labels for all selected IDs in normal product usage. name submits repeated ID values, read with FormData.getAll(name). disabled disables both opening and removal. For custom validation, pass aria-invalid and aria-describedby, and associate a visible label with the trigger's id.

The trigger and chip-removal buttons are siblings, so no buttons are nested. Each removal action has a specific accessible name. A polite status announces the selection count. This is a separate component built from Combobox; no search-placement switch was added to its root API.

Native form reset restores the initial defaultValue for uncontrolled MultiSelect and respects a cancelled reset event. Controlled values remain owned by the caller; handle the form's onReset to reset your controlled state.

Inline search alternative

Keep inline search when a small number of selections and continuous typing suit the product:

Select several frameworks. Remove a chip to deselect it.

const anchor = useComboboxAnchor();
const [values, setValues] = useState<string[]>(['Next.js']);

<Combobox items={frameworks} multiple value={values} onValueChange={setValues}>
  <ComboboxChips ref={anchor}>
    <ComboboxValue>
      {values.map(value => <ComboboxChip key={value} aria-label={value}>{value}</ComboboxChip>)}
    </ComboboxValue>
    <ComboboxChipsInput aria-label="Supported frameworks" placeholder="Add a framework…" />
  </ComboboxChips>
  <ComboboxContent anchor={anchor}>
    <ComboboxEmpty>No frameworks found.</ComboboxEmpty>
    <ComboboxList>
      {(framework) => <ComboboxItem key={framework} value={framework}>{framework}</ComboboxItem>}
    </ComboboxList>
  </ComboboxContent>
</Combobox>

Import the additional ComboboxChips, ComboboxChip, ComboboxChipsInput, ComboboxValue, and useComboboxAnchor exports from the same module, and useState from React. The anchor aligns the popup to the whole chips container. Chip controls remove selected values; they do not delete anything outside the form.

Forms

Typing alone does not commit a selection.

Choose a result and submit to inspect its form value.

Set name on the root to submit the selected value. Custom validation should validate selection, not just search text, and focus the input when invalid. The demo uses a controlled selection with explicit validation. Arbitrary typed text does not become a submitted selection; editing an already-selected label may retain the previous committed value until another option is selected or Clear is used. For multiple values, read FormData.getAll(name). Disabled comboboxes are omitted from submission. Validate allowed values and permissions again on the server.

Groups and accessibility

ComboboxGroup, ComboboxLabel, ComboboxCollection, and ComboboxSeparator are available for structured option collections; use Base UI's grouped items structure and a nested ComboboxCollection rather than manually filtering away its item context.

Arrow keys move through results, Enter selects the highlighted result, and Escape closes the popup. Focus stays on the editable input while the highlighted result is communicated through aria-activedescendant. Tab leaves the control. Clear and chip removal are keyboard-accessible buttons. ComboboxEmpty remains mounted as a polite live region so no-results updates can be announced. Motion preferences are respected.

Based on shadcn Combobox and Base UI Combobox.

On this page