MultiSelect
Multi-pick combobox. Selected items render as removable badges in the trigger; the dropdown lists every option with a searchable, checkable row.
Installation
import { MultiSelect } from "@gradeui/ui"Usage
Examples
With per-option icons
The icon renders both in the dropdown row and on the selected badge.
Search hidden
For short lists, pass searchable={false} so the dropdown opens straight to the options.
Disabled
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| options | { value: string; label: string; icon?: ComponentType; disabled?: boolean }[] | — | The full pool of selectable items. |
| value | string[] | — | Controlled selection. When set, onValueChange must be wired. |
| defaultValue | string[] | [] | Uncontrolled initial selection. |
| onValueChange | (next: string[]) => void | — | Fired with the full next selection on every change. |
| placeholder | string | "Select…" | Trigger text when nothing is selected. |
| maxCount | number | 3 | How many selected badges to show on the trigger before collapsing to "+N more". |
| searchable | boolean | true | Show the search input in the dropdown. Hide for short option lists. |
| badgeDismissible | boolean | true | Show the × button on each selected badge. |
| disabled | boolean | false | Disable the whole control. |
Accessibility
- Trigger is
role="combobox"witharia-expandedwired to the open state. - Each removable badge is a focusable
role="button"with anaria-labelso screen-reader users can remove items without opening the dropdown. - Selection inside the dropdown is keyboard-driven via Command (cmdk): ↑/↓ to navigate, ↵ to toggle, Esc to close.
When NOT to use
For single-pick reach for <Select>. For unbounded / async lists (users to mention, search-as-you-type API results) use <Command>directly — MultiSelect’s options model expects the full set up front.
Sidecar
The Markdown sidecar Studio (and the Grade MCP server, when it ships) reads to understand this component — frontmatter, when- to-use guidance, and canonical examples. Authored once at packages/ui/components/ui/multi-select.md and shipped inside the published @gradeui/ui tarball.
---
name: MultiSelect
import: "@gradeui/ui"
props:
- options: { value: string; label: string; icon?: ComponentType; disabled?: boolean }[]
- value?: string[] — controlled selection
- defaultValue?: string[] — uncontrolled initial selection
- onValueChange?: (next: string[]) => void
- placeholder?: string (default "Select…")
- searchPlaceholder?: string (default "Search…")
- emptyMessage?: string (default "Nothing matches.")
- maxCount?: number (default 3) — badges shown on the trigger before collapsing to "+N more"
- searchable?: boolean (default true) — hide for short option lists
- badgeDismissible?: boolean (default true) — show × on each selected badge
- disabled?: boolean
- modalPopover?: boolean (default false) — Popover modal mode
- className?: string
when_to_use: Picking multiple items from a finite list — tag selectors, filter chips, "share with N people", multi-region settings. The trigger renders the current selection as removable Badges so the choice is always visible. For SINGLE selection use Select. For huge unbounded sets (users, autocompleted email addresses) reach for Command directly with custom rendering.
composes_with: [Popover, Command, Badge, Checkbox-style row indicator, Separator]
aliases: [multi select, multiselect, multi-select, tag picker, chips input, multi picker, multi-pick combobox, multipicker, tag select, react native multi select, multi-select combobox]
---
```jsx
const frameworks = [
{ value: "next", label: "Next.js" },
{ value: "remix", label: "Remix" },
{ value: "astro", label: "Astro" },
{ value: "nuxt", label: "Nuxt" },
];
<MultiSelect
options={frameworks}
defaultValue={["next", "remix"]}
onValueChange={setSelected}
placeholder="Pick frameworks"
maxCount={2}
/>
```
```jsx
// With per-option icons — the icon renders both in the dropdown row
// and on the selected badge.
import { Code2, Server, Cloud } from "lucide-react";
const services = [
{ value: "edge", label: "Edge runtime", icon: Cloud },
{ value: "node", label: "Node runtime", icon: Server },
{ value: "browser", label: "Browser only", icon: Code2 },
];
<MultiSelect options={services} placeholder="Select runtimes" />
```
### Anti-patterns
DO NOT use MultiSelect for single-pick — that's `<Select>`. The visual semantics differ (badges vs single value) and screen-reader announcements differ ("combobox, 2 selected" vs "combobox, Apple").
DO NOT pass `value` without `onValueChange` — the component becomes a read-only display of the controlled state and selections inside the popover silently no-op. Either go fully uncontrolled (`defaultValue`) or wire both.
DO NOT inline `options` as `[{value, label}, ...]` from scratch on every render — memoise it. The component memoises its internal lookup, but a fresh array reference on every parent render still forces React to reconcile every row.
DO NOT reach for MultiSelect when the list is unbounded or async (users to mention, email recipients, search-as-you-type API results). Use `<Command>` directly with custom rendering — MultiSelect's `options` model expects the full set up front.