Multi Select
ProA select that picks many values and stays open while you toggle them. Radix Select closes on pick, so this is built on Radix Popover instead, matching Select's surface with a checkbox or switch per row.
Installation#
Usage#
The preview is yours to explore. Its code comes with a Pro license, paid once and used in every project.
Have a license? Unlock it here
Anatomy#
import {
MultiSelect,
MultiSelectTrigger,
MultiSelectValue,
MultiSelectContent,
MultiSelectGroup,
MultiSelectLabel,
MultiSelectItem,
MultiSelectSwitchItem,
MultiSelectSeparator,
} from "@/components/ui/multi-select"
<MultiSelect>
<MultiSelectTrigger>
<MultiSelectValue />
</MultiSelectTrigger>
<MultiSelectContent>
<MultiSelectGroup>
<MultiSelectLabel />
<MultiSelectItem value="…" />
<MultiSelectSwitchItem value="…" />
</MultiSelectGroup>
<MultiSelectSeparator />
</MultiSelectContent>
</MultiSelect>Checkbox items#
MultiSelectItem is the default row: a leading checkbox, a label, and an optional icon. Clicking anywhere on the row toggles it, and the menu stays open so you can pick several. Each value is added to or removed from the selection array passed to onValueChange. Set disabled on a row to lock it.
Switch items#
MultiSelectSwitchItem swaps the checkbox for the DS Switch, giving the menu a settings-panel feel for on/off rows like notification channels. It shares the same selection state as MultiSelectItem, so the two can be mixed.
Rendering the value#
By default MultiSelectValue shows a count (“3 selected”), robust because the trigger has no access to the row labels while the panel is closed. The number rolls as you toggle rows (“3 selected” → “4 selected”) via AnimatedNumber, so the change registers instead of hard-swapping. Pass renderValue to draw the selection yourself, e.g. as Badge chips mapped from your own data. The chips carry that same on-change polish: each one zoom + fades in when selected and out when removed, so the row never hard-swaps.
Sections#
Wrap related rows in MultiSelectGroup with a MultiSelectLabel heading, and split sections with MultiSelectSeparator: the same grouping model as Select. See the checkbox example above for a two-section menu.
Density#
Trigger height comes from size on MultiSelect (sm/md/lg = 32/36/40px, the same scale as Button, Input, and Select). density no longer sets the height; it picks the default size when you do not pass one (compact → sm, comfortable → md) and tightens the menu rows - flowing to every part through context, or from a parent DensityProvider.
API reference#
MultiSelect
The root that owns the string[] selection, over Radix Popover.Root (forwards open, defaultOpen, onOpenChange, modal). Every part accepts className, merged last.
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string[] | - | The selection, controlled. |
| defaultValue | string[] | [] | The initial selection, uncontrolled. |
| onValueChange | (value: string[]) => void | - | Called with the full next array whenever a row is toggled. |
| size | "sm" | "md" | "lg" | - | Trigger height 32, 36 or 40px. Unset, it comes from a container (Form imposes lg) or density (compact, the default, is sm). |
| density | "comfortable" | "compact" | - | Row padding in the panel, and the default size. Inherits the nearest DensityProvider when unset. |
MultiSelectTrigger
The field-shaped button with a trailing caret. Forwards Radix Popover.Trigger props.
| Data attribute | Description |
|---|---|
| data-state | open or closed. |
MultiSelectValue
The trigger's text: the placeholder, or a rolling count like 3 selected. Forwards native <span> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| placeholder | string | - | Shown when nothing is selected. |
| renderValue | (value: string[]) => React.ReactNode | - | Draws the selection yourself, e.g. Badge chips mapped from your own labels. |
MultiSelectContent
The panel, role="listbox" with aria-multiselectable, portalled to the body; arrow keys, Home and End move between rows. Forwards Radix Popover.Content props.
| Prop | Type | Default | Description |
|---|---|---|---|
| align | "start" | "center" | "end" | "start" | Alignment against the trigger. |
| sideOffset | number | 6 | Gap from the trigger. |
| collisionPadding | number | 8 | Keeps the panel off the viewport edge. |
| Data attribute | Description |
|---|---|
| data-state | open or closed, for the enter and exit animation. |
| data-side | The side the panel landed on. |
| CSS variable | Description |
|---|---|
| --radix-popover-trigger-width | The panel's minimum width, so it never runs narrower than the trigger. |
| --radix-popover-content-available-height | Caps the scrolling list at the room left in the viewport (20rem at most). |
MultiSelectItem
A row, role="option", with the label first and a decorative Checkbox on the right; Enter or Space toggles it. Forwards native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | - | The value this row adds to or removes from the selection. |
| disabled | boolean | - | Skips the row in the arrow-key rotation and blocks toggling. |
MultiSelectSwitchItem
The same row with a Switch in place of the Checkbox, for a settings feel. Forwards native <div> props.
| Prop | Type | Default | Description |
|---|---|---|---|
| value* | string | - | The value this row adds to or removes from the selection. |
| disabled | boolean | - | Skips the row in the arrow-key rotation and blocks toggling. |
MultiSelectGroup
Groups related rows, role="group". Forwards native <div> props.
MultiSelectLabel
A muted heading over a group. Forwards native <div> props.
MultiSelectSeparator
A hairline between groups. Forwards native <div> props.