Maksud UI
Components

Multi Select

A multi-select dropdown component that allows users to select multiple options from a list with checkboxes.

API
'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-react

Make sure you have the following components installed:

Copy 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

PropDescription
optionsArray of options to display. Each option has label and value properties.
selectedArray of selected option values.
onChangeCallback when selection changes.
placeholderPlaceholder text shown in the trigger button.
emptyMessageMessage displayed when options array is empty.
iconOptional icon component to display next to selected items.
classNameAdditional CSS classes for the trigger button.

MultiSelectOption

PropDescription
labelDisplay text for the option.
valueUnique 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