Multi Select

Pro

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#

# One-time setup (tokens + lib helpers)npx koalaui-cli@latest init# Activate your Pro license (once per machine)npx koalaui-cli@latest login <key># Add this component (its dependencies come along)npx koalaui-cli@latest add multi-select

Usage#

This example is part of Koala UI Pro

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.

PropTypeDefaultDescription
valuestring[]-The selection, controlled.
defaultValuestring[][]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 attributeDescription
data-stateopen or closed.

MultiSelectValue

The trigger's text: the placeholder, or a rolling count like 3 selected. Forwards native <span> props.

PropTypeDefaultDescription
placeholderstring-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.

PropTypeDefaultDescription
align"start" | "center" | "end""start"Alignment against the trigger.
sideOffsetnumber6Gap from the trigger.
collisionPaddingnumber8Keeps the panel off the viewport edge.
Data attributeDescription
data-stateopen or closed, for the enter and exit animation.
data-sideThe side the panel landed on.
CSS variableDescription
--radix-popover-trigger-widthThe panel's minimum width, so it never runs narrower than the trigger.
--radix-popover-content-available-heightCaps 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.

PropTypeDefaultDescription
value*string-The value this row adds to or removes from the selection.
disabledboolean-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.

PropTypeDefaultDescription
value*string-The value this row adds to or removes from the selection.
disabledboolean-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.

FAQ#

Radix Select is strictly single-value and closes on pick, so it cannot host checkbox rows or stay open while you toggle. MultiSelect uses Radix Popover styled to match `SelectContent`, and each row toggles membership in one `string[]` instead of committing a single value.