Components
Multi Select
A multi-select dropdown component that allows users to select multiple options from a list with checkboxes.
'use client';
import * as React from 'react';
import { MultiSelect } from '@/components/ui/multi-select';
export function MultiSelectDemo() {
const [selected, setSelected] = React.useState<string[]>([]);
const frameworks = [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Angular', value: 'angular' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Ember', value: 'ember' },
];
return (
<div className='w-full max-w-md'>
<MultiSelect
options={frameworks}
selected={selected}
onChange={setSelected}
placeholder='Frameworks'
/>
</div>
);
}Installation
CLI
npx shadcn@latest add "https://maksud.dev/r/multi-select"Manual
Install the following dependencies:
npm install lucide-reactCopy and paste the following code into your project.
'use client';
import { Checkbox } from '@/components/ui/checkbox';
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
import { cn } from '@/lib/utils';
import { Button } from '@/components/ui/button';
import { ChevronDown, X } from 'lucide-react';
import * as React from 'react';
export interface MultiSelectOption {
label: string;
value: string;
}
interface MultiSelectProps {
options: MultiSelectOption[];
selected: string[];
onChange: (selected: string[]) => void;
placeholder?: string;
className?: string;
emptyMessage?: string;
icon?: React.ComponentType<{ className?: string }>;
}
function MultiSelect({
options,
selected,
onChange,
placeholder = 'Select items',
className,
emptyMessage = 'No items found',
icon: Icon,
}: MultiSelectProps) {
const [open, setOpen] = React.useState(false);
function handleToggle(value: string) {
const newSelected = selected.includes(value)
? selected.filter((item) => item !== value)
: [...selected, value];
onChange(newSelected);
}
function handleClearAll(e: React.MouseEvent) {
e.stopPropagation();
onChange([]);
}
const displayText = React.useMemo(() => {
if (selected.length === 0) {
return placeholder;
}
const count = selected.length;
return `${placeholder} (${count})`;
}, [selected, placeholder]);
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
variant='outline'
role='combobox'
aria-expanded={open}
className={cn('w-full justify-between', className)}
>
<span className='truncate'>{displayText}</span>
<div className='ml-2 flex items-center gap-1'>
{selected.length > 0 && (
<button
type='button'
onClick={handleClearAll}
className='rounded-sm p-0.5 transition-colors hover:bg-accent'
>
<X className='h-3.5 w-3.5 opacity-50 hover:opacity-100' />
</button>
)}
<ChevronDown className='h-4 w-4 shrink-0 opacity-50' />
</div>
</Button>
</PopoverTrigger>
<PopoverContent className='w-full p-0' align='start'>
<div className='max-h-80 overflow-y-auto p-1'>
{options.length === 0 ? (
<div className='py-6 text-center text-muted-foreground text-sm'>{emptyMessage}</div>
) : (
<div className='space-y-0.5' role='listbox'>
{options.map((option) => {
const isSelected = selected.includes(option.value);
return (
<div
key={option.value}
role='option'
aria-selected={isSelected}
className={cn(
'flex cursor-pointer items-center gap-2 rounded-md px-3 py-1.5 transition-colors hover:bg-accent',
isSelected && 'bg-accent/50 font-medium'
)}
onClick={() => handleToggle(option.value)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleToggle(option.value);
}
}}
tabIndex={0}
>
<Checkbox
checked={isSelected}
onCheckedChange={() => handleToggle(option.value)}
onClick={(e) => e.stopPropagation()}
tabIndex={-1}
/>
<span className='flex-1 text-sm'>{option.label}</span>
{isSelected && Icon && <Icon className='h-4 w-4 text-primary' />}
</div>
);
})}
</div>
)}
</div>
{selected.length > 0 && (
<div className='border-t px-2 py-1.5'>
<Button variant='ghost' size='sm' onClick={handleClearAll} className='w-full'>
Clear all ({selected.length})
</Button>
</div>
)}
</PopoverContent>
</Popover>
);
}
export { MultiSelect };Layout
Import the component and use it in your application.
import { MultiSelect } from "@/components/ui/multi-select";
export default function Example() {
const [selected, setSelected] = React.useState<string[]>([]);
const options = [
{ label: "React", value: "react" },
{ label: "Vue", value: "vue" },
{ label: "Angular", value: "angular" },
];
return (
<MultiSelect
options={options}
selected={selected}
onChange={setSelected}
placeholder="Select frameworks"
/>
);
}Examples
Basic
'use client';
import * as React from 'react';
import { MultiSelect } from '@/components/ui/multi-select';
export function MultiSelectDemo() {
const [selected, setSelected] = React.useState<string[]>([]);
const frameworks = [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Angular', value: 'angular' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Ember', value: 'ember' },
];
return (
<div className='w-full max-w-md'>
<MultiSelect
options={frameworks}
selected={selected}
onChange={setSelected}
placeholder='Frameworks'
/>
</div>
);
}A simple multi-select with checkboxes and clear functionality.
With Icons
'use client';
import { Check } from 'lucide-react';
import * as React from 'react';
import { MultiSelect } from '@/components/ui/multi-select';
export function MultiSelectWithIconsDemo() {
const [selected, setSelected] = React.useState<string[]>(['react', 'vue']);
const frameworks = [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Angular', value: 'angular' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Ember', value: 'ember' },
{ label: 'Next.js', value: 'nextjs' },
];
return (
<div className='w-full max-w-md'>
<MultiSelect
options={frameworks}
selected={selected}
onChange={setSelected}
placeholder='Frameworks'
icon={Check}
/>
</div>
);
}Display a check icon next to selected items using the icon prop.
Controlled
'use client';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import { MultiSelect } from '@/components/ui/multi-select';
export function MultiSelectControlledDemo() {
const [selected, setSelected] = React.useState<string[]>(['react']);
const frameworks = [
{ label: 'React', value: 'react' },
{ label: 'Vue', value: 'vue' },
{ label: 'Angular', value: 'angular' },
{ label: 'Svelte', value: 'svelte' },
{ label: 'Ember', value: 'ember' },
];
function selectAll() {
setSelected(frameworks.map((f) => f.value));
}
function clearAll() {
setSelected([]);
}
return (
<div className='w-full max-w-md space-y-4'>
<MultiSelect
options={frameworks}
selected={selected}
onChange={setSelected}
placeholder='Frameworks'
/>
<div className='flex gap-2'>
<Button variant='outline' size='sm' onClick={selectAll}>
Select All
</Button>
<Button variant='outline' size='sm' onClick={clearAll}>
Clear All
</Button>
</div>
<div className='rounded-md border p-3'>
<p className='text-muted-foreground text-sm'>
Selected: {selected.length === 0 ? 'None' : selected.join(', ')}
</p>
</div>
</div>
);
}Full control over the selected state with external controls.
API Reference
MultiSelect
| Prop | Description |
|---|---|
options | Array of options to display. Each option has label and value properties. |
selected | Array of selected option values. |
onChange | Callback when selection changes. |
placeholder | Placeholder text shown in the trigger button. |
emptyMessage | Message displayed when options array is empty. |
icon | Optional icon component to display next to selected items. |
className | Additional CSS classes for the trigger button. |
MultiSelectOption
| Prop | Description |
|---|---|
label | Display text for the option. |
value | Unique identifier for the option. |
Accessibility
- Uses proper ARIA roles (
combobox,listbox,option) for screen reader support - Keyboard navigation with Enter and Space keys to toggle selections
- Focus management with visible focus indicators
- Selected state is properly announced to assistive technologies
- Supports tab navigation through all interactive elements