- Notifications
You must be signed in to change notification settings - Fork81
A multi-select component designed with shadcn/ui
License
sersavan/shadcn-multi-select-component
Folders and files
Name | Name | Last commit message | Last commit date | |
---|---|---|---|---|
Repository files navigation
A powerful and flexible multi-select component built withReact,TypeScript,Tailwind CSS, andshadcn/ui components.
Compatible with: Next.js, Vite, Create React App, and any React environmentthat supports path aliases and shadcn/ui components.
- ✨Multiple Variants: Default, secondary, destructive, and inverted styles
- 🌈Custom Styling: Custom badge colors, icon colors, and gradientbackgrounds
- 📁Grouped Options: Organize options in groups with headings andseparators
- 🚫Disabled Options: Mark specific options as disabled and non-selectable
- 🎨Advanced Animations: Multiple animation types (bounce, pulse, wiggle,fade, slide) for badges and popovers
- 🔍Search & Filter: Built-in search functionality with keyboard navigation
- 📊Dashboard Integration: Perfect for analytics dashboards and datavisualization
- 📈Chart Filtering: Real-time filtering for bar, pie, area, and linecharts
- 🎯Multi-level Filtering: Primary and secondary filter combinations
- 📱Responsive Design: Automatic adaptation to mobile, tablet, and desktopscreens
- 📐Width Constraints: Support for minimum and maximum width settings
- 📲Mobile-Optimized: Compact mode with touch-friendly interactions
- 💻Desktop-Enhanced: Full feature set with large displays
- ♿Accessibility: Full keyboard support, screen reader compatibility, andARIA live regions
- ⌨️Keyboard Shortcuts: Global hotkeys for navigation and quick actions
- 🔧Imperative Methods: Programmatic control via ref (reset, clear, focus,get/set values)
- 🔄Duplicate Handling: Automatic detection and removal of duplicateoptions
- 📝Form Integration: Seamless integration with React Hook Form andvalidation
- 🎛️Customizable Behavior: Auto-close on select, width constraints, emptyindicators
- 🎯TypeScript Support: Fully typed with comprehensive TypeScript support
- 📦Self-Contained: All accessibility features built-in, no externaldependencies required
This component is compatible with any React project but requires proper setup:
- React environment: Next.js, Vite, Create React App, or any React setup
- Path aliases: Configure
@/
imports in your bundler - shadcn/ui: Install and configure shadcn/ui components
- Tailwind CSS: Setup and configure Tailwind CSS
cp src/components/multi-select.tsx your-project/components/
npm install react react-domnpm install @radix-ui/react-popover @radix-ui/react-separatornpm install lucide-react class-variance-authority clsx tailwind-merge cmdk
Install the required shadcn/ui components:
npx shadcn@latest add button badge popovercommand separator
Add to yourtsconfig.json
orjsconfig.json
:
{"compilerOptions": {"baseUrl":".","paths": {"@/*": ["./src/*"]}}}
Add to yourvite.config.ts
:
import{defineConfig}from"vite";importpathfrom"path";exportdefaultdefineConfig({resolve:{alias:{"@":path.resolve(__dirname,"./src"),},},});
You may need to eject or use CRACO to configure path aliases.
Ensure you have thecn
utility function insrc/lib/utils.ts
:
import{typeClassValue,clsx}from"clsx";import{twMerge}from"tailwind-merge";exportfunctioncn(...inputs:ClassValue[]){returntwMerge(clsx(inputs));}
import{MultiSelect}from"@/components/multi-select";import{useState}from"react";constoptions=[{value:"react",label:"React"},{value:"vue",label:"Vue.js"},{value:"angular",label:"Angular"},];functionApp(){const[selectedValues,setSelectedValues]=useState<string[]>([]);return(<MultiSelectoptions={options}onValueChange={setSelectedValues}defaultValue={selectedValues}/>);}
conststyledOptions=[{value:"react",label:"React",icon:ReactIcon,style:{badgeColor:"#61DAFB",iconColor:"#282C34",},},{value:"vue",label:"Vue.js",icon:VueIcon,style:{gradient:"linear-gradient(135deg, #4FC08D 0%, #42B883 100%)",},},];<MultiSelectoptions={styledOptions}onValueChange={setSelected}placeholder="Select with custom styles..."/>;
For Next.js projects, you can use the component in client components with the"use client" directive:
"use client";import{MultiSelect}from"@/components/multi-select";import{useState}from"react";constoptions=[{value:"next",label:"Next.js"},{value:"react",label:"React"},{value:"typescript",label:"TypeScript"},];exportdefaultfunctionMyPage(){const[selected,setSelected]=useState<string[]>([]);return(<divclassName="container mx-auto p-4"><h1className="text-2xl font-bold mb-4">Select Technologies</h1><MultiSelectoptions={options}onValueChange={setSelected}placeholder="Select technologies..."variant="secondary"/></div>);}
constgroupedOptions=[{heading:"Frontend Frameworks",options:[{value:"react",label:"React"},{value:"vue",label:"Vue.js"},{value:"angular",label:"Angular",disabled:true},],},{heading:"Backend Technologies",options:[{value:"node",label:"Node.js"},{value:"python",label:"Python"},],},];<MultiSelectoptions={groupedOptions}onValueChange={setSelected}placeholder="Select from groups..."/>;
The Multi-Select component includes comprehensive responsive design capabilitiesthat automatically adapt to different screen sizes.
Enable responsive design with default settings:
<MultiSelectoptions={options}onValueChange={setSelected}responsive={true}// Enable automatic responsive behaviorplaceholder="Responsive component"/>
Default responsive settings:
- Mobile (< 640px): 2 badges max, compact mode
- Tablet (640px - 1024px): 4 badges max, normal mode
- Desktop (> 1024px): 6 badges max, full features
Control component width with responsive adaptation:
<MultiSelectoptions={options}onValueChange={setSelected}responsive={true}minWidth="200px"maxWidth="400px"placeholder="Constrained width"/>
The Multi-Select component provides powerful integration with analyticsdashboards and data visualization libraries.
import{MultiSelect}from"@/components/multi-select";import{BarChart,Bar,XAxis,YAxis,ResponsiveContainer}from"recharts";constDashboard=()=>{const[selectedCategories,setSelectedCategories]=useState(["2024"]);constfilteredData=data.filter((item)=>selectedCategories.includes(item.category));return(<divclassName="space-y-4"><MultiSelectoptions={[{value:"2024",label:"2024",icon:CalendarIcon},{value:"2023",label:"2023",icon:CalendarIcon},]}onValueChange={setSelectedCategories}defaultValue={selectedCategories}placeholder="Select time period"responsive={true}/><ResponsiveContainerwidth="100%"height={300}><BarChartdata={filteredData}><XAxisdataKey="name"/><YAxis/><BardataKey="value"fill="#8884d8"/></BarChart></ResponsiveContainer></div>);};
constAdvancedDashboard=()=>{const[primaryFilters,setPrimaryFilters]=useState(["Performance"]);const[secondaryFilters,setSecondaryFilters]=useState(["Speed"]);return(<divclassName="space-y-4"><divclassName="grid grid-cols-2 gap-4"><MultiSelectoptions={primaryCategories}onValueChange={setPrimaryFilters}placeholder="Primary category"/><MultiSelectoptions={secondaryCategories}onValueChange={setSecondaryFilters}placeholder="Secondary filters"variant="secondary"/></div><ComposedChartdata={filteredData}>{/* Multiple chart types combined */}</ComposedChart></div>);};
const[selectedValues,setSelectedValues]=useState<string[]>([]);<MultiSelectoptions={frameworks}onValueChange={setSelectedValues}defaultValue={selectedValues}placeholder="Select frameworks"/>;
const[selectedValues,setSelectedValues]=useState<string[]>([]);<MultiSelectoptions={frameworksWithIcons}onValueChange={setSelectedValues}defaultValue={selectedValues}placeholder="Select technologies"variant="inverted"maxCount={3}modalPopover={true}/>;
const[options,setOptions]=useState<Option[]>([]);const[loading,setLoading]=useState(true);useEffect(()=>{loadData().then((data)=>{setOptions(data);setLoading(false);});},[]);<MultiSelectoptions={options}onValueChange={setSelectedValues}defaultValue={selectedValues}placeholder={loading ?"Loading..." :"Select items"}disabled={loading}/>;
- Always provide meaningful labels: Use descriptive placeholders andaria-labels
- Handle loading states: Show loading indicators when fetching async data
- Limit display count: Use
maxCount
for large selections to maintain UIclarity - Use modal on mobile: Set
modalPopover={true}
for better mobileexperience - Implement search: For large option lists, consider adding searchfunctionality
- Provide clear feedback: Use the built-in announcements for screen readers
Prop | Type | Default | Description |
---|---|---|---|
options | MultiSelectOption[] | MultiSelectGroup[] | - | Array of selectable options or groups |
onValueChange | (value: string[]) => void | - | Callback when selection changes |
defaultValue | string[] | [] | Initially selected values |
placeholder | string | "Select options" | Placeholder text |
variant | "default" | "secondary" | "destructive" | "inverted" | "default" | Visual variant |
animation | number | 0 | Legacy animation duration in seconds |
animationConfig | AnimationConfig | - | Advanced animation configuration |
maxCount | number | 3 | Maximum visible selected items |
modalPopover | boolean | false | Modal behavior for popover |
asChild | boolean | false | Render as child component |
className | string | - | Additional CSS classes |
hideSelectAll | boolean | false | Hide "Select All" option |
searchable | boolean | true | Enable search functionality |
emptyIndicator | ReactNode | - | Custom empty state component |
autoSize | boolean | false | Allow component to grow/shrink with content |
singleLine | boolean | false | Show badges in single line with scroll |
popoverClassName | string | - | Custom CSS class for popover content |
disabled | boolean | false | Disable the entire component |
responsive | boolean | ResponsiveConfig | false | Enable responsive behavior |
minWidth | string | - | Minimum component width |
maxWidth | string | - | Maximum component width |
deduplicateOptions | boolean | false | Automatically remove duplicate options |
resetOnDefaultValueChange | boolean | true | Reset state when defaultValue changes |
closeOnSelect | boolean | false | Close popover after selecting an option |
interfaceAnimationConfig{badgeAnimation?:"bounce"|"pulse"|"wiggle"|"fade"|"slide"|"none";popoverAnimation?:"scale"|"slide"|"fade"|"flip"|"none";optionHoverAnimation?:"highlight"|"scale"|"glow"|"none";duration?:number;// Animation duration in secondsdelay?:number;// Animation delay in seconds}
interfaceMultiSelectRef{reset:()=>void;// Reset to default valuegetSelectedValues:()=>string[];// Get current selectionsetSelectedValues:(values:string[])=>void;// Set selection programmaticallyclear:()=>void;// Clear all selectionsfocus:()=>void;// Focus the component}
interfaceMultiSelectOption{label:string;// Display textvalue:string;// Unique identifiericon?:React.ComponentType<{// Optional icon componentclassName?:string;}>;disabled?:boolean;// Whether option is disabledstyle?:{// Custom stylingbadgeColor?:string;// Custom badge background coloriconColor?:string;// Custom icon colorgradient?:string;// Gradient background (CSS gradient)};}
interfaceMultiSelectGroup{heading:string;// Group heading textoptions:MultiSelectOption[];// Options in this group}
The MultiSelect component includes comprehensive accessibility featuresbuilt-in:
- ARIA Live Regions: Automatic announcements for screen readers whenselections change
- Role Attributes: Proper
combobox
,listbox
, andoption
roles - ARIA Labels: Descriptive labels for all interactive elements
- ARIA States:
aria-expanded
,aria-selected
,aria-disabled
states
- Tab: Focus component and navigate between elements
- Enter/Space: Open dropdown and select options
- Arrow Keys: Navigate through options
- Escape: Close dropdown
- Backspace: Remove last selected item when search is empty
The component automatically announces:
- Number of options selected
- When dropdown opens/closes
- Search results count
- Individual option selection/deselection
<MultiSelectoptions={options}onValueChange={setSelected}placeholder="Choose frameworks (accessible)"// Accessibility features are built-in and automaticsearchable={true}aria-label="Framework selection"/>
// The component provides imperative methods for testingconstmultiSelectRef=useRef<MultiSelectRef>(null);// Programmatic focus for testingmultiSelectRef.current?.focus();// Check current selectionconstcurrentValues=multiSelectRef.current?.getSelectedValues();
// Single color badge with custom icon color{value:"react",label:"React",style:{badgeColor:"#61DAFB",iconColor:"#282C34"}}// Gradient badge (icon will be white by default){value:"vue",label:"Vue.js",style:{gradient:"linear-gradient(135deg, #4FC08D 0%, #42B883 100%)"}}// Multiple gradients{value:"angular",label:"Angular",style:{gradient:"linear-gradient(45deg, #DD0031 0%, #C3002F 50%, #FF6B6B 100%)"}}
constbrandColors={react:{badgeColor:"#61DAFB",iconColor:"#282C34"},vue:{gradient:"linear-gradient(135deg, #4FC08D 0%, #42B883 100%)"},angular:{badgeColor:"#DD0031",iconColor:"#ffffff"},svelte:{gradient:"linear-gradient(135deg, #FF3E00 0%, #FF8A00 100%)"},node:{badgeColor:"#339933",iconColor:"#ffffff"},};
// Wiggle animation with custom timing<MultiSelectoptions={options}onValueChange={setSelected}animationConfig={{badgeAnimation:"wiggle",duration:0.5,}}/>// Pulse animation with delay<MultiSelectoptions={options}onValueChange={setSelected}animationConfig={{badgeAnimation:"pulse",popoverAnimation:"fade",duration:0.3,delay:0.1,}}/>// Scale animation for popover<MultiSelectoptions={options}onValueChange={setSelected}animationConfig={{badgeAnimation:"slide",popoverAnimation:"scale",duration:0.4,}}/>
<MultiSelectoptions={options}onValueChange={setSelected}animationConfig={{badgeAnimation:"wiggle",popoverAnimation:"scale",duration:0.3,delay:0.1,}}placeholder="Animated component"/>
import{useRef}from"react";importtype{MultiSelectRef}from"@/components/multi-select";functionControlledExample(){constmultiSelectRef=useRef<MultiSelectRef>(null);consthandleReset=()=>{multiSelectRef.current?.reset();};consthandleClear=()=>{multiSelectRef.current?.clear();};consthandleSelectAll=()=>{constallValues=options.map((option)=>option.value);multiSelectRef.current?.setSelectedValues(allValues);};consthandleFocus=()=>{multiSelectRef.current?.focus();};return(<divclassName="space-y-4"><MultiSelectref={multiSelectRef}options={options}onValueChange={setSelected}placeholder="Controlled component"/><divclassName="flex gap-2"><buttononClick={handleReset}>Reset</button><buttononClick={handleClear}>Clear</button><buttononClick={handleSelectAll}>Select All</button><buttononClick={handleFocus}>Focus</button></div></div>);}
<MultiSelectoptions={options}onValueChange={setSelected}closeOnSelect={true}placeholder="Closes after each selection"/>
constoptionsWithDuplicates=[{value:"react",label:"React"},{value:"react",label:"React Duplicate"},// Will be handled{value:"vue",label:"Vue.js"},];<MultiSelectoptions={optionsWithDuplicates}onValueChange={setSelected}deduplicateOptions={true}// Automatically removes duplicatesplaceholder="Handles duplicates"/>;
import{useForm,Controller}from"react-hook-form";import{zodResolver}from"@hookform/resolvers/zod";import{z}from"zod";constformSchema=z.object({technologies:z.array(z.string()).min(1,"Select at least one technology"),});functionMyForm(){constform=useForm({resolver:zodResolver(formSchema),defaultValues:{technologies:[]},});return(<formonSubmit={form.handleSubmit(onSubmit)}><Controllercontrol={form.control}name="technologies"render={({ field})=>(<MultiSelectoptions={techOptions}onValueChange={field.onChange}defaultValue={field.value}placeholder="Select technologies..."/>)}/></form>);}
functionControlledExample(){const[selected,setSelected]=useState<string[]>([]);constselectRandom=()=>{constrandomItems=options.filter((item)=>!item.disabled).sort(()=>0.5-Math.random()).slice(0,3).map((item)=>item.value);setSelected(randomItems);};return(<div><MultiSelectoptions={options}onValueChange={setSelected}defaultValue={selected}/><buttononClick={selectRandom}>Random Selection</button><buttononClick={()=>setSelected([])}>Clear All</button></div>);}
<MultiSelectoptions={options}onValueChange={setSelected}searchable={true}emptyIndicator={<divclassName="flex flex-col items-center p-4"><SearchIconclassName="h-8 w-8 text-muted-foreground mb-2"/><pclassName="text-muted-foreground">No items found</p><pclassName="text-xs text-muted-foreground">Try a different search term</p></div>}/>
constcomplexStructure=[{heading:"Frontend Frameworks",options:[{value:"react",label:"React",icon:ReactIcon,style:{badgeColor:"#61DAFB",iconColor:"#282C34"},},{value:"vue",label:"Vue.js",icon:VueIcon,style:{gradient:"linear-gradient(135deg, #4FC08D 0%, #42B883 100%)",},},{value:"angular",label:"Angular",icon:AngularIcon,disabled:true,style:{badgeColor:"#DD0031",iconColor:"#ffffff"},},],},{heading:"State Management",options:[{value:"redux",label:"Redux"},{value:"zustand",label:"Zustand"},{value:"recoil",label:"Recoil",disabled:true},],},];
- Technology Stack Selection: Choose programming languages, frameworks,libraries
- Skill Assessment: Multi-skill selection for profiles or job applications
- Category Filtering: Filter content by multiple categories
- Tag Management: Select multiple tags for articles or products
- Permission Management: Assign multiple roles or permissions
- Geographic Selection: Choose multiple countries, regions, or locations
- Product Configuration: Select features, variants, or add-ons
- Team Assignment: Assign multiple team members to projects
The component uses CSS classes that can be customized via Tailwind CSS. You canoverride styles by passing custom classes:
<MultiSelectclassName="my-custom-class"options={options}onValueChange={setSelected}/>
Create your own variants by extending themultiSelectVariants
:
constcustomVariants=cva("base-classes",{variants:{variant:{// ... existing variantspremium:"bg-gradient-to-r from-purple-500 to-pink-500 text-white",minimal:"bg-transparent border-dashed",},},});
The component automatically adapts to your theme (light/dark mode) when usingthe shadcn/ui theme provider.
The component is fully typed and provides excellent TypeScript support:
// All types are exported for useimporttype{MultiSelectOption,MultiSelectGroup,MultiSelectProps,MultiSelectRef,AnimationConfig,}from"@/components/multi-select";// Type-safe option creationconstcreateOption=(value:string,label:string,options?:Partial<MultiSelectOption>):MultiSelectOption=>({value,label,...options,});// Type-safe event handlersconsthandleChange=(values:string[])=>{// values is automatically typed as string[]console.log("Selected:",values);};
# Clone the repositorygit clone https://github.com/sersavan/shadcn-multi-select-component.git# Navigate to the projectcd shadcn-multi-select-component# Install dependenciesnpm install# Start the development servernpm run dev# Open your browser to http://localhost:3000
├── src/│ ├── app/│ │ ├── page.tsx # Demo page with examples│ │ ├── layout.tsx # Root layout│ │ └── globals.css # Global styles│ ├── components/│ │ ├── multi-select.tsx # Main component│ │ ├── icons.tsx # Icon components│ │ └── ui/ # shadcn/ui components│ └── lib/│ └── utils.ts # Utility functions├── components.json # shadcn/ui config├── tailwind.config.ts # Tailwind configuration└── package.json # Dependencies and scripts
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
This project is licensed under the MIT License - see theLICENSE filefor details.
- Built withshadcn/ui
- Icons fromLucide React
- Powered byRadix UI
- Styled withTailwind CSS
Check out the live demo:Multi-Select Component Demo
Made with ❤️ bysersavan
About
A multi-select component designed with shadcn/ui
Topics
Resources
License
Uh oh!
There was an error while loading.Please reload this page.
Stars
Watchers
Forks
Releases
Packages0
Uh oh!
There was an error while loading.Please reload this page.