Skip to content

sersavan/shadcn-multi-select-component

Repository files navigation

Star History

Star History Chart

Shadcn Multi Select Component

A powerful and flexible multi-select component built with React, TypeScript, Tailwind CSS, and shadcn/ui components.

Compatible with: Next.js, Vite, Create React App, and any React environment that supports path aliases and shadcn/ui components.

Multi Select Demo TypeScript Tailwind CSS

πŸš€ Features

  • ✨ Multiple Variants: Default, secondary, destructive, and inverted styles
  • 🌈 Custom Styling: Custom badge colors, icon colors, and gradient backgrounds
  • πŸ“ Grouped Options: Organize options in groups with headings and separators
  • 🚫 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 data visualization
  • πŸ“ˆ Chart Filtering: Real-time filtering for bar, pie, area, and line charts
  • 🎯 Multi-level Filtering: Primary and secondary filter combinations
  • πŸ“± Responsive Design: Automatic adaptation to mobile, tablet, and desktop screens
  • πŸ“ 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 and screen reader compatibility
  • πŸ”§ Imperative Methods: Programmatic control via ref (reset, clear, focus, get/set values)
  • πŸ”„ Duplicate Handling: Automatic detection and removal of duplicate options
  • πŸ“ Form Integration: Seamless integration with React Hook Form and validation
  • πŸŽ›οΈ Customizable Behavior: Auto-close on select, width constraints, empty indicators
  • 🎯 TypeScript Support: Fully typed with comprehensive TypeScript support
  • πŸ“¦ Zero Dependencies: Only uses peer dependencies you already have

πŸ“¦ Installation

Prerequisites

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

1. Copy the Component

cp src/components/multi-select.tsx your-project/components/

2. Install Dependencies

npm install react react-dom
npm install @radix-ui/react-popover @radix-ui/react-separator
npm install lucide-react class-variance-authority clsx tailwind-merge cmdk

3. Setup shadcn/ui Components

Install the required shadcn/ui components:

npx shadcn@latest add button badge popover command separator

4. Configure Path Aliases

For Next.js

Add to your tsconfig.json or jsconfig.json:

{
	"compilerOptions": {
		"baseUrl": ".",
		"paths": {
			"@/*": ["./src/*"]
		}
	}
}

For Vite

Add to your vite.config.ts:

import { defineConfig } from "vite";
import path from "path";

export default defineConfig({
	resolve: {
		alias: {
			"@": path.resolve(__dirname, "./src"),
		},
	},
});

For Webpack/Create React App

You may need to eject or use CRACO to configure path aliases.

5. Setup Utility Function

Ensure you have the cn utility function in src/lib/utils.ts:

import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
	return twMerge(clsx(inputs));
}

🎯 Quick Start

Basic Usage

import { MultiSelect } from "@/components/multi-select";
import { useState } from "react";

const options = [
	{ value: "react", label: "React" },
	{ value: "vue", label: "Vue.js" },
	{ value: "angular", label: "Angular" },
];

function App() {
	const [selectedValues, setSelectedValues] = useState<string[]>([]);

	return (
		<MultiSelect
			options={options}
			onValueChange={setSelectedValues}
			defaultValue={selectedValues}
		/>
	);
}

Custom Styling

const styledOptions = [
	{
		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%)",
		},
	},
];

<MultiSelect
	options={styledOptions}
	onValueChange={setSelected}
	placeholder="Select with custom styles..."
/>;

Next.js Usage

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";

const options = [
	{ value: "next", label: "Next.js" },
	{ value: "react", label: "React" },
	{ value: "typescript", label: "TypeScript" },
];

export default function MyPage() {
	const [selected, setSelected] = useState<string[]>([]);

	return (
		<div className="container mx-auto p-4">
			<h1 className="text-2xl font-bold mb-4">Select Technologies</h1>
			<MultiSelect
				options={options}
				onValueChange={setSelected}
				placeholder="Select technologies..."
				variant="secondary"
			/>
		</div>
	);
}

Grouped Options

const groupedOptions = [
	{
		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" },
		],
	},
];

<MultiSelect
	options={groupedOptions}
	onValueChange={setSelected}
	placeholder="Select from groups..."
/>;

οΏ½ Responsive Design

The Multi-Select component includes comprehensive responsive design capabilities that automatically adapt to different screen sizes.

Automatic Responsive Behavior

Enable responsive design with default settings:

<MultiSelect
	options={options}
	onValueChange={setSelected}
	responsive={true} // Enable automatic responsive behavior
	placeholder="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

Width Constraints

Control component width with responsive adaptation:

<MultiSelect
	options={options}
	onValueChange={setSelected}
	responsive={true}
	minWidth="200px"
	maxWidth="400px"
	placeholder="Constrained width"
/>

πŸ“Š Dashboard Integration

The Multi-Select component provides powerful integration with analytics dashboards and data visualization libraries.

Basic Dashboard Filtering

import { MultiSelect } from "@/components/multi-select";
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer } from "recharts";

const Dashboard = () => {
	const [selectedCategories, setSelectedCategories] = useState(["2024"]);

	const filteredData = data.filter((item) =>
		selectedCategories.includes(item.category)
	);

	return (
		<div className="space-y-4">
			<MultiSelect
				options={[
					{ value: "2024", label: "2024", icon: CalendarIcon },
					{ value: "2023", label: "2023", icon: CalendarIcon },
				]}
				onValueChange={setSelectedCategories}
				defaultValue={selectedCategories}
				placeholder="Select time period"
				responsive={true}
			/>

			<ResponsiveContainer width="100%" height={300}>
				<BarChart data={filteredData}>
					<XAxis dataKey="name" />
					<YAxis />
					<Bar dataKey="value" fill="#8884d8" />
				</BarChart>
			</ResponsiveContainer>
		</div>
	);
};

Multi-level Filtering

const AdvancedDashboard = () => {
	const [primaryFilters, setPrimaryFilters] = useState(["Performance"]);
	const [secondaryFilters, setSecondaryFilters] = useState(["Speed"]);

	return (
		<div className="space-y-4">
			<div className="grid grid-cols-2 gap-4">
				<MultiSelect
					options={primaryCategories}
					onValueChange={setPrimaryFilters}
					placeholder="Primary category"
				/>

				<MultiSelect
					options={secondaryCategories}
					onValueChange={setSecondaryFilters}
					placeholder="Secondary filters"
					variant="secondary"
				/>
			</div>

			<ComposedChart data={filteredData}>
				{/* Multiple chart types combined */}
			</ComposedChart>
		</div>
	);
};

πŸ“š API Reference

Props

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

Types

AnimationConfig

interface AnimationConfig {
	badgeAnimation?: "bounce" | "pulse" | "wiggle" | "fade" | "slide" | "none";
	popoverAnimation?: "scale" | "slide" | "fade" | "flip" | "none";
	optionHoverAnimation?: "highlight" | "scale" | "glow" | "none";
	duration?: number; // Animation duration in seconds
	delay?: number; // Animation delay in seconds
}

MultiSelectRef

interface MultiSelectRef {
	reset: () => void; // Reset to default value
	getSelectedValues: () => string[]; // Get current selection
	setSelectedValues: (values: string[]) => void; // Set selection programmatically
	clear: () => void; // Clear all selections
	focus: () => void; // Focus the component
}

MultiSelectOption

interface MultiSelectOption {
	label: string; // Display text
	value: string; // Unique identifier
	icon?: React.ComponentType<{
		// Optional icon component
		className?: string;
	}>;
	disabled?: boolean; // Whether option is disabled
	style?: {
		// Custom styling
		badgeColor?: string; // Custom badge background color
		iconColor?: string; // Custom icon color
		gradient?: string; // Gradient background (CSS gradient)
	};
}

MultiSelectGroup

interface MultiSelectGroup {
	heading: string; // Group heading text
	options: MultiSelectOption[]; // Options in this group
}

🎨 Styling Examples

Custom Colors

// 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%)"
  }
}

Brand Colors

const brandColors = {
	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" },
};

Animation Examples

// Wiggle animation with custom timing
<MultiSelect
  options={options}
  onValueChange={setSelected}
  animationConfig={{
    badgeAnimation: "wiggle",
    duration: 0.5,
  }}
/>

// Pulse animation with delay
<MultiSelect
  options={options}
  onValueChange={setSelected}
  animationConfig={{
    badgeAnimation: "pulse",
    popoverAnimation: "fade",
    duration: 0.3,
    delay: 0.1,
  }}
/>

// Scale animation for popover
<MultiSelect
  options={options}
  onValueChange={setSelected}
  animationConfig={{
    badgeAnimation: "slide",
    popoverAnimation: "scale",
    duration: 0.4,
  }}
/>

🎯 Advanced Examples

Advanced Animations

<MultiSelect
	options={options}
	onValueChange={setSelected}
	animationConfig={{
		badgeAnimation: "wiggle",
		popoverAnimation: "scale",
		duration: 0.3,
		delay: 0.1,
	}}
	placeholder="Animated component"
/>

Programmatic Control via Ref

import { useRef } from "react";
import type { MultiSelectRef } from "@/components/multi-select";

function ControlledExample() {
	const multiSelectRef = useRef<MultiSelectRef>(null);

	const handleReset = () => {
		multiSelectRef.current?.reset();
	};

	const handleClear = () => {
		multiSelectRef.current?.clear();
	};

	const handleSelectAll = () => {
		const allValues = options.map((option) => option.value);
		multiSelectRef.current?.setSelectedValues(allValues);
	};

	const handleFocus = () => {
		multiSelectRef.current?.focus();
	};

	return (
		<div className="space-y-4">
			<MultiSelect
				ref={multiSelectRef}
				options={options}
				onValueChange={setSelected}
				placeholder="Controlled component"
			/>
			<div className="flex gap-2">
				<button onClick={handleReset}>Reset</button>
				<button onClick={handleClear}>Clear</button>
				<button onClick={handleSelectAll}>Select All</button>
				<button onClick={handleFocus}>Focus</button>
			</div>
		</div>
	);
}

Auto-close on Select

<MultiSelect
	options={options}
	onValueChange={setSelected}
	closeOnSelect={true}
	placeholder="Closes after each selection"
/>

Duplicate Handling

const optionsWithDuplicates = [
	{ value: "react", label: "React" },
	{ value: "react", label: "React Duplicate" }, // Will be handled
	{ value: "vue", label: "Vue.js" },
];

<MultiSelect
	options={optionsWithDuplicates}
	onValueChange={setSelected}
	deduplicateOptions={true} // Automatically removes duplicates
	placeholder="Handles duplicates"
/>;

With Form Integration (React Hook Form)

import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const formSchema = z.object({
	technologies: z.array(z.string()).min(1, "Select at least one technology"),
});

function MyForm() {
	const form = useForm({
		resolver: zodResolver(formSchema),
		defaultValues: { technologies: [] },
	});

	return (
		<form onSubmit={form.handleSubmit(onSubmit)}>
			<Controller
				control={form.control}
				name="technologies"
				render={({ field }) => (
					<MultiSelect
						options={techOptions}
						onValueChange={field.onChange}
						defaultValue={field.value}
						placeholder="Select technologies..."
					/>
				)}
			/>
		</form>
	);
}

Programmatic Control

function ControlledExample() {
	const [selected, setSelected] = useState<string[]>([]);

	const selectRandom = () => {
		const randomItems = options
			.filter((item) => !item.disabled)
			.sort(() => 0.5 - Math.random())
			.slice(0, 3)
			.map((item) => item.value);
		setSelected(randomItems);
	};

	return (
		<div>
			<MultiSelect
				options={options}
				onValueChange={setSelected}
				defaultValue={selected}
			/>
			<button onClick={selectRandom}>Random Selection</button>
			<button onClick={() => setSelected([])}>Clear All</button>
		</div>
	);
}

Custom Empty State

<MultiSelect
	options={options}
	onValueChange={setSelected}
	searchable={true}
	emptyIndicator={
		<div className="flex flex-col items-center p-4">
			<SearchIcon className="h-8 w-8 text-muted-foreground mb-2" />
			<p className="text-muted-foreground">No items found</p>
			<p className="text-xs text-muted-foreground">
				Try a different search term
			</p>
		</div>
	}
/>

Complex Grouped Structure

const complexStructure = [
	{
		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 },
		],
	},
];

🎯 Use Cases

  • 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

πŸ› οΈ Customization

Style Customization

The component uses CSS classes that can be customized via Tailwind CSS. You can override styles by passing custom classes:

<MultiSelect
	className="my-custom-class"
	options={options}
	onValueChange={setSelected}
/>

Custom Variants

Create your own variants by extending the multiSelectVariants:

const customVariants = cva("base-classes", {
	variants: {
		variant: {
			// ... existing variants
			premium: "bg-gradient-to-r from-purple-500 to-pink-500 text-white",
			minimal: "bg-transparent border-dashed",
		},
	},
});

Theme Integration

The component automatically adapts to your theme (light/dark mode) when using the shadcn/ui theme provider.

πŸ“ TypeScript Support

The component is fully typed and provides excellent TypeScript support:

// All types are exported for use
import type {
	MultiSelectOption,
	MultiSelectGroup,
	MultiSelectProps,
	MultiSelectRef,
	AnimationConfig,
} from "@/components/multi-select";

// Type-safe option creation
const createOption = (
	value: string,
	label: string,
	options?: Partial<MultiSelectOption>
): MultiSelectOption => ({
	value,
	label,
	...options,
});

// Type-safe event handlers
const handleChange = (values: string[]) => {
	// values is automatically typed as string[]
	console.log("Selected:", values);
};

πŸš€ Getting Started

Clone and Run

# Clone the repository
git clone https://github.com/sersavan/shadcn-multi-select-component.git

# Navigate to the project
cd shadcn-multi-select-component

# Install dependencies
npm install

# Start the development server
npm run dev

# Open your browser to http://localhost:3000

Project Structure

β”œβ”€β”€ 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

🀝 Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

πŸ“„ License

This project is licensed under the MIT License - see the LICENSE file for details.

πŸ™ Acknowledgments

πŸš€ Live Demo

Check out the live demo: Multi-Select Component Demo


Made with ❀️ by sersavan