Headless / Selection

MultiSelect

MultiSelect picks several values. It pairs a filterable listbox with a row of removable chips — click to toggle, Backspace on an empty input to drop the last chip.

Loading playground…
Updated 3 days agoEdit on GitHubWeb & native

What it is

A compound component holding a value array. MultiSelect.Root owns the selection, the filter text, and the open state. MultiSelect.Chips renders the current selection as removable chips; MultiSelect.Input is the type-to-filter field; MultiSelect.List is the multi-selectable listbox. MultiSelect.SelectAll is an optional toggle for the whole filtered set.

Selection is toggle-on-click — clicking a selected option removes it, and the list stays open so the user can keep picking. maxSelections caps the array; once it is full, further additions are ignored.

Install

example.tsx
yarn add @usemotif/headless
example.tsx
import { MultiSelect } from '@usemotif/headless';

Anatomy

example.tsx
<MultiSelect.Root options={options} value={value} onValueChange={setValue}>
<MultiSelect.Chips
  renderChip={(option, { remove }) => (
    <Chip label={option.label} onRemove={remove} />
  )}
/>
<MultiSelect.Input placeholder="Add…" />
<MultiSelect.List />
</MultiSelect.Root>

API

MultiSelect.Root

MultiSelect.Root

stable
function MultiSelect.Root<T>(props: MultiSelectRootProps<T>): JSX.Element
optionsreadonly ComboboxOption<T>[]required

The option set — each `{ value, label, disabled? }`.

value / defaultValue / onValueChangereadonly T[] / readonly T[] / (next) => void

The selected values, controlled or uncontrolled.

inputValue / onInputValueChangestring / (next: string) => void

The filter text.

filter(option, input) => boolean

Custom filter predicate. Defaults to a case-insensitive substring match on `label`.

maxSelectionsnumber

Cap on the number of selected values. Additions past the cap are a no-op.

enableSelectAllboolean= false

Surface a `MultiSelect.SelectAll` toggle that selects every non-disabled option in the current filter.

MultiSelect.Chips

MultiSelect.Chips

stable
function MultiSelect.Chips<T>(props: { renderChip: (option, info) => ReactNode }): JSX.Element

Renders one node per selected value. renderChip receives the option and an info object with a remove callback and the chip's index.

MultiSelect.Input

The type-to-filter field. Backspace on an empty input removes the last chip — the standard chip-input affordance.

MultiSelect.List

The multi-selectable listbox — role="listbox" with aria-multiselectable="true". Takes the same renderOption, placement, offset, and emptyMessage props as Combobox.List.

MultiSelect.SelectAll

A toggle for the filtered set. Requires enableSelectAll on the Root. It clones its child with role="checkbox" and an aria-checked of true, false, or "mixed".

Accessibility

The input is role="combobox"; the listbox is role="listbox" with aria-multiselectable="true" and aria-selected on each row. The keyboard model matches Combobox — arrow keys, Home, End — except Enter toggles an option without closing the list, since the user is likely picking more. Each chip needs a remove control with an accessible name, e.g. an icon button labelled "Remove TypeScript".

Examples

A tag picker with a cap:

example.tsx
<MultiSelect.Root
options={allTags}
value={tags}
onValueChange={setTags}
maxSelections={5}
>
<MultiSelect.Chips
  renderChip={(opt, { remove }) => (
    <HStack gap="$1" bg="$colors.surface.muted" px="$2" borderRadius="$radii.full">
      <Text>{opt.label}</Text>
      <IconButton aria-label={`Remove ${opt.label}`} size="xs" onPress={remove}>
        <CloseIcon />
      </IconButton>
    </HStack>
  )}
/>
<MultiSelect.Input placeholder="Add a tag…" />
<MultiSelect.List />
</MultiSelect.Root>