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.
| Prop | Type | Default | Description |
|---|---|---|---|
multiple | boolean | false | Selects several values. The value becomes an array. |
items | readonly any[] | — | Options to filter. Spread useComboboxSelectedGroups to group. |
filter | (item, query) => boolean | — | Custom match. useComboboxSelectedGroups passes one. |
value | string[] | — | Controlled selected values. |
defaultValue | string[] | — | 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. |
disabled | boolean | false | Disables the trigger and hides ComboboxClear. |
readOnly | boolean | false | Blocks 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.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "soft" | "surface" | "outlined" | "ghost" | Field / Theme | Input style override. |
className | string | — | Merges with the trigger styles. |
ComboboxChips
One row of chips that never wraps. The function child renders one chip per pick.
| Prop | Type | Default | Description |
|---|---|---|---|
children | (item: Item, index: number) => ReactNode | — | Renders one ComboboxChip per pick, in pick order. |
max | number | — | 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. |
placeholder | ReactNode | — | Shown while nothing is picked: text or a ComboboxAddChip. |
className | string | — | 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.
| Prop | Type | Default | Description |
|---|---|---|---|
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. |
sideOffset | number | 4 | Gap between the trigger and the popup, in pixels. |
container | HTMLElement | null | — | Mount the portal into a different document or element than document.body. |
className | string | — | 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
comboboxbutton named by theFieldLabel(oraria-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
maxhides 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,ComboboxInputandComboboxClear; the icons are decorative. - With
max, hidden picks are announced only as a count. The "Selected" group in the popup lists all of them. ComboboxClearhastabIndex={-1}, so it is not reachable with Tab; keyboard users remove picks in the popup.