Combobox

A Field that picks several values from a searchable list and shows the picks as one row of chips.

Usage

import {
  Combobox,
  ComboboxChip,
  ComboboxChips,
  ComboboxClear,
  ComboboxClose,
  ComboboxCollection,
  ComboboxContent,
  ComboboxEmpty,
  ComboboxGroup,
  ComboboxGroupLabel,
  ComboboxInput,
  ComboboxItem,
  ComboboxItemIndicator,
  ComboboxList,
  ComboboxTrigger,
  ComboboxTriggerGroup,
  type ComboboxSelectedGroup,
  useComboboxSelectedGroups,
} from '@epilot/spark-ui/combobox'
import { Field, FieldLabel } from '@epilot/spark-ui/field'
function DevicePicker() {
  const [value, setValue] = React.useState<string[]>([])
  const groups = useComboboxSelectedGroups(devices, value, {
    selected: `Selected · ${value.length}`,
    items: 'Devices',
  })
  return (
    <Field>
      <FieldLabel>Devices</FieldLabel>
      <Combobox multiple value={value} onValueChange={setValue} {...groups}>
        <ComboboxTriggerGroup>
          <ComboboxTrigger>
            <ComboboxChips max={2} placeholder="Select devices">
              {(device: string) => (
                <ComboboxChip key={device}>{device}</ComboboxChip>
              )}
            </ComboboxChips>
          </ComboboxTrigger>
          <ComboboxClear aria-label="Clear devices" />
        </ComboboxTriggerGroup>
        <ComboboxContent aria-label="Devices">
          <ComboboxInput aria-label="Search devices" placeholder="Search" />
          <ComboboxEmpty>No devices found</ComboboxEmpty>
          <ComboboxList>
            {(group: ComboboxSelectedGroup<string>) => (
              <ComboboxGroup key={group.value} items={group.items}>
                <ComboboxGroupLabel>{group.label}</ComboboxGroupLabel>
                <ComboboxCollection>
                  {(device: string) => (
                    <ComboboxItem key={device} value={device}>
                      {device}
                      <ComboboxItemIndicator className="ml-auto" />
                    </ComboboxItem>
                  )}
                </ComboboxCollection>
              </ComboboxGroup>
            )}
          </ComboboxList>
          <ComboboxClose>Done</ComboboxClose>
        </ComboboxContent>
      </Combobox>
    </Field>
  )
}

Use Combobox when the user picks several values from a long list and needs to type to find them. The value is a string[], the same shape as Select with multiple. Reach for Autocomplete when the user picks one value or may type free text, and for Select with multiple when the list is short enough to scan without searching.

The field is the trigger: it shows the picks as one row of Pill chips that never wraps, so the field keeps its height. The search sits at the top of the popup. useComboboxSelectedGroups splits the list into two groups: every pick under "Selected", which the search never filters, then the remaining options, which it does. Uncheck a pick in "Selected" to remove it.

Empty, status, overflow, group and button labels are yours to pass. Combobox ships no English defaults except the ComboboxChips overflow label.

Examples

Label position

ComboboxTrigger takes the same Field chrome as Select: outset puts the label above, inset puts it inside the shell.

Loading

Render ComboboxStatus while results load. It is a polite live region, so screen readers announce the copy. Hide ComboboxEmpty during the load so "No devices found" does not flash.

Capped chips

Chips past the field's width are clipped. Pass max to ComboboxChips to render at most that many chips, then a "+N" pill for the rest; the example at the top of the page uses max={2}. The "+N" pill is not focusable; screen readers hear "N more selected". Every pick stays listed under "Selected" in the popup.

Summary

Render ComboboxValue in the trigger instead of ComboboxChips to show a count. Use it when picks are many and their names matter less than the total.

Add chip

Pass a ComboboxAddChip as the ComboboxChips placeholder to show a dashed "+ Add device" chip while nothing is picked. It is part of the trigger, not a button of its own.

Disabled

Set data-disabled on the Field and disabled on the Combobox root. Chips stay visible and ComboboxClear hides, so the disabled field shows no dead button.

On a phone

Below the sm breakpoint the popup opens as a bottom sheet: full width, anchored to the bottom of the screen over a dimmed backdrop, with the search on top, each item's ComboboxItemIndicator drawn as a checkbox, and ComboboxClose as a full-width "Done" button. From sm up, ComboboxClose hides and the popup closes on an outside press. Resize the window to see it.

API Reference

Built on Base UI Combobox. See the Base UI docs for the full API (items, value, onValueChange, inputValue, onInputValueChange, itemToStringLabel, isItemEqualToValue, useFilter, …). Spark notes:

Combobox

Root state. Pass multiple for a string[] value.

PropTypeDefaultDescription
multiplebooleanfalseSelects several values. The value becomes an array.
itemsreadonly any[]—Options to filter. Spread useComboboxSelectedGroups to group.
filter(item, query) => boolean—Custom match. useComboboxSelectedGroups passes one.
valuestring[]—Controlled selected values.
defaultValuestring[]—Uncontrolled initial selected values.
onValueChange(value: string[]) => void—Fires on pick, uncheck, Backspace, and clear. Clear passes [].
onInputValueChange(inputValue: string) => void—Fires as the user types. Use it to start an async search.
disabledbooleanfalseDisables the trigger and hides ComboboxClear.
readOnlybooleanfalseBlocks edits and hides ComboboxClear.
itemToStringLabel(item: any) => string—Maps an object item to its label. Required for non-string items.

useComboboxSelectedGroups

useComboboxSelectedGroups(items, value, { selected, items, isItemEqualToValue }) returns { items, filter, isItemEqualToValue } to spread on Combobox. items holds two ComboboxSelectedGroup objects, { value, label, items, optionCount }: every pick first, then the options not picked. filter matches the search but always keeps picks. It needs a controlled value. Empty groups are not rendered. For object items whose picks are not the same references as items, pass isItemEqualToValue, for example (item, picked) => item.id === picked.id; it defaults to Object.is.

ComboboxTriggerGroup

Wraps ComboboxTrigger and ComboboxClear. The trigger is a native <button>, so the clear button sits beside it, overlaid at its end, rather than nested inside it.

ComboboxTrigger

The field. A combobox button that opens the popup; it renders its children and a chevron. Takes the same chrome as Select's trigger in every inputStyle and label position.

PropTypeDefaultDescription
variant"soft" | "surface" | "outlined" | "ghost"Field / ThemeInput style override.
classNamestring—Merges with the trigger styles.

ComboboxChips

One row of chips that never wraps. The function child renders one chip per pick.

PropTypeDefaultDescription
children(item: Item, index: number) => ReactNode—Renders one ComboboxChip per pick, in pick order.
maxnumber—Renders the first max chips, then a non-interactive "+N" pill.
overflowLabel(count: number) => string"N more selected"Screen-reader name of the "+N" pill. Pass a translated string.
placeholderReactNode—Shown while nothing is picked: text or a ComboboxAddChip.
classNamestring—Merges with the row styles.

ComboboxChip / ComboboxAddChip

ComboboxChip is one pick, rendered as a static Pill that truncates long labels. ComboboxAddChip is a dashed Pill with a plus icon. Both are display-only, because they sit inside the trigger button.

ComboboxInput

The search at the top of the popup, with a search icon. Pass aria-label. Backspace in the empty search removes the last pick.

ComboboxContent

Renders its own portal and positions the popup under the trigger, matching its width. Below sm it is a bottom sheet. It is a dialog, so pass aria-label.

PropTypeDefaultDescription
side"top" | "bottom" | "left" | "right""bottom"Side of the trigger to place the popup. Ignored in the bottom sheet.
align"start" | "center" | "end""start"Alignment along the chosen side.
sideOffsetnumber4Gap between the trigger and the popup, in pixels.
containerHTMLElement | null—Mount the portal into a different document or element than document.body.
classNamestring—Merges with the popup styles.

ComboboxClose

The bottom sheet's "Done" button. It closes the popup and hides from sm up. Pass its label as children.

ComboboxList / ComboboxItem / ComboboxItemIndicator

ComboboxList takes a function child over items (or over groups). ComboboxItem is one option; value must match an entry in items. ComboboxItemIndicator shows a check mark while its item is selected, and an empty or checked box in the bottom sheet. Add className="ml-auto" to push it to the end of the row.

ComboboxGroup / ComboboxGroupLabel / ComboboxCollection / ComboboxSeparator

Section a grouped list, as in Autocomplete. Pass each group's items to ComboboxGroup and render them with ComboboxCollection.

ComboboxEmpty / ComboboxStatus

ComboboxEmpty renders its children when the filtered list is empty. With useComboboxSelectedGroups it also renders them when the search matches no option, even though the picks under "Selected" stay listed. ComboboxStatus is a live region for loading or result counts. Pass the copy from your app.

ComboboxClear

Icon button beside the trigger. Childless, it renders a close icon; pass an aria-label. It mounts only when something is picked, empties the value to [] without opening the popup, returns focus to the trigger, and hides when the Combobox is disabled or read-only.

ComboboxValue

Reads the selected values. A function child receives the array, for a summary such as "4 selected".

useComboboxFilter / useComboboxFilteredItems

Re-exports of Base UI Combobox.useFilter and Combobox.useFilteredItems for async or custom filtering.

Accessibility

  • The trigger is a combobox button named by the FieldLabel (or aria-label). Enter, Space, or ArrowDown opens the popup and moves focus to the search.
  • The popup is a dialog holding the search and a multi-select listbox. Arrow keys move the highlight, Enter toggles the highlighted option, Escape closes the popup and returns focus to the trigger.
  • Backspace in the empty search removes the last pick, even when max hides its chip behind "+N".
  • Chips, the add chip and the "+N" pill are static text inside the trigger, so there are no nested interactive controls. Remove a pick by unchecking it under "Selected", with Backspace, or with ComboboxClear.
  • Label ComboboxContent, ComboboxInput and ComboboxClear; the icons are decorative.
  • With max, hidden picks are announced only as a count. The "Selected" group in the popup lists all of them.
  • ComboboxClear has tabIndex={-1}, so it is not reachable with Tab; keyboard users remove picks in the popup.

On this page