Multi Select
A 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#
import { Code, PenNib } from "@phosphor-icons/react"
import {
MultiSelect, MultiSelectTrigger, MultiSelectValue, MultiSelectContent,
MultiSelectItem, MultiSelectSwitchItem,
MultiSelectGroup, MultiSelectLabel, MultiSelectSeparator,
} from "@/components/ui/multi-select"
export function Example() {
const [value, setValue] = useState<string[]>([])
return (
<MultiSelect value={value} onValueChange={setValue}>
<MultiSelectTrigger className="w-64">
<MultiSelectValue placeholder="Select categories" />
</MultiSelectTrigger>
<MultiSelectContent>
<MultiSelectItem value="engineering"><Code /> Engineering</MultiSelectItem>
<MultiSelectItem value="design"><PenNib /> Design</MultiSelectItem>
</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.