react-multiselect-radixui
v1.0.0
Published
A modern, accessible React multi-select component with search functionality and visible selected tabs, built with TypeScript and Tailwind CSS.
Readme
React Multi-Select with Tabs
A modern, accessible React multi-select component with search functionality and visible selected tabs, built with TypeScript and Tailwind CSS.
Features
- 🎯 Multi-select with visible tags - Selected items appear as removable tags
- 🔍 Search functionality - Filter options with real-time search
- ⌨️ Keyboard navigation - Full keyboard support with arrow keys, Enter, Escape
- 🎨 Tailwind CSS styling - Beautiful, customizable design
- ♿ Accessible - ARIA compliant and screen reader friendly
- 📱 Responsive - Works great on all screen sizes
- 🔧 TypeScript - Full TypeScript support with proper types
- 🎛️ Highly customizable - Extensive customization options
Installation
npm install react-multi-select-tabsMake sure you have Tailwind CSS installed and configured in your project.
Basic Usage
import React, { useState } from 'react';
import { MultiSelect, Option } from 'react-multi-select-tabs';
const options: Option[] = [
{ value: '1', label: 'Apple' },
{ value: '2', label: 'Banana' },
{ value: '3', label: 'Cherry' },
{ value: '4', label: 'Date' },
{ value: '5', label: 'Elderberry' }
];
function App() {
const [selectedValues, setSelectedValues] = useState<(string | number)[]>([]);
return (
<div className="max-w-md mx-auto p-6">
<label className="block text-sm font-medium text-gray-700 mb-2">
Select Fruits
</label>
<MultiSelect
options={options}
value={selectedValues}
onChange={setSelectedValues}
placeholder="Choose your favorite fruits..."
searchPlaceholder="Search fruits..."
/>
</div>
);
}
export default App;Props
| Prop | Type | Default | Description |
|------|------|---------|-------------|
| options | Option[] | [] | Array of options to display |
| value | (string \| number)[] | [] | Currently selected values |
| onChange | (values: (string \| number)[]) => void | - | Callback when selection changes |
| placeholder | string | "Select options..." | Placeholder when no items selected |
| searchPlaceholder | string | "Search..." | Placeholder for search input |
| disabled | boolean | false | Disable the entire component |
| maxSelectedItems | number | - | Maximum number of items that can be selected |
| className | string | "" | Additional CSS classes for container |
| dropdownClassName | string | "" | Additional CSS classes for dropdown |
| tagClassName | string | "" | Additional CSS classes for selected tags |
| searchInputClassName | string | "" | Additional CSS classes for search input |
| noOptionsMessage | string | "No options found" | Message when no options match search |
| closeOnSelect | boolean | false | Close dropdown after selecting an item |
| clearable | boolean | true | Show clear all button |
| loading | boolean | false | Show loading state |
| loadingMessage | string | "Loading..." | Loading message text |
Option Interface
interface Option {
value: string | number;
label: string;
disabled?: boolean;
}Advanced Examples
With Maximum Selection Limit
<MultiSelect
options={options}
value={selectedValues}
onChange={setSelectedValues}
maxSelectedItems={3}
placeholder="Select up to 3 items"
/>With Custom Styling
<MultiSelect
options={options}
value={selectedValues}
onChange={setSelectedValues}
className="w-full"
tagClassName="bg-green-100 text-green-800"
dropdownClassName="border-2 border-green-200"
searchInputClassName="text-lg"
/>With Loading State
<MultiSelect
options={options}
value={selectedValues}
onChange={setSelectedValues}
loading={isLoading}
loadingMessage="Fetching options..."
/>Keyboard Navigation
- Arrow Down/Up: Navigate through options
- Enter: Select highlighted option or open dropdown
- Escape: Close dropdown
- Backspace: Remove last selected item when search is empty
Styling
The component uses Tailwind CSS classes and can be customized by:
- Overriding CSS classes with the various
classNameprops - Using Tailwind's configuration to customize colors and spacing
- CSS custom properties for advanced theming
Browser Support
- Chrome (latest)
- Firefox (latest)
- Safari (latest)
- Edge (latest)
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
License
MIT License - see the LICENSE file for details.
Now I'll create a demo component to showcase the library:
<lov-write file_path="src/pages/Index.tsx">
import React, { useState } from 'react';
import { MultiSelect } from '../index';
import type { Option } from '../types';
const fruitOptions: Option[] = [
{ value: 'apple', label: 'Apple' },
{ value: 'banana', label: 'Banana' },
{ value: 'cherry', label: 'Cherry' },
{ value: 'date', label: 'Date' },
{ value: 'elderberry', label: 'Elderberry' },
{ value: 'fig', label: 'Fig' },
{ value: 'grape', label: 'Grape' },
{ value: 'honeydew', label: 'Honeydew' },
{ value: 'kiwi', label: 'Kiwi' },
{ value: 'lemon', label: 'Lemon' },
{ value: 'mango', label: 'Mango' },
{ value: 'orange', label: 'Orange' },
{ value: 'papaya', label: 'Papaya' },
{ value: 'quince', label: 'Quince' },
{ value: 'raspberry', label: 'Raspberry' },
{ value: 'strawberry', label: 'Strawberry' },
];
const countryOptions: Option[] = [
{ value: 'us', label: 'United States' },
{ value: 'ca', label: 'Canada' },
{ value: 'mx', label: 'Mexico' },
{ value: 'uk', label: 'United Kingdom' },
{ value: 'fr', label: 'France' },
{ value: 'de', label: 'Germany' },
{ value: 'it', label: 'Italy' },
{ value: 'es', label: 'Spain' },
{ value: 'jp', label: 'Japan' },
{ value: 'au', label: 'Australia' },
];
const Index = () => {
const [selectedFruits, setSelectedFruits] = useState<(string | number)[]>(['apple', 'banana']);
const [selectedCountries, setSelectedCountries] = useState<(string | number)[]>([]);
const [limitedSelection, setLimitedSelection] = useState<(string | number)[]>([]);
const [loading, setLoading] = useState(false);
const handleLoadingDemo = () => {
setLoading(true);
setTimeout(() => setLoading(false), 2000);
};
return (
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100 py-12 px-4">
<div className="max-w-4xl mx-auto">
{/* Header */}
<div className="text-center mb-12">
<h1 className="text-4xl font-bold text-gray-900 mb-4">
React Multi-Select with Tabs
</h1>
<p className="text-xl text-gray-600 max-w-2xl mx-auto">
A modern, accessible multi-select component with search functionality and visible selected tabs
</p>
</div>
{/* Demo Grid */}
<div className="grid gap-8 md:grid-cols-2">
{/* Basic Example */}
<div className="bg-white rounded-xl shadow-lg p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4">Basic Example</h2>
<div className="space-y-3">
<label className="block text-sm font-medium text-gray-700">
Select Your Favorite Fruits
</label>
<MultiSelect
options={fruitOptions}
value={selectedFruits}
onChange={setSelectedFruits}
placeholder="Choose fruits..."
searchPlaceholder="Search fruits..."
/>
<div className="text-sm text-gray-500">
Selected: {selectedFruits.length} items
</div>
</div>
</div>
{/* Countries Example */}
<div className="bg-white rounded-xl shadow-lg p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4">Countries Selection</h2>
<div className="space-y-3">
<label className="block text-sm font-medium text-gray-700">
Select Countries
</label>
<MultiSelect
options={countryOptions}
value={selectedCountries}
onChange={setSelectedCountries}
placeholder="Choose countries..."
searchPlaceholder="Search countries..."
closeOnSelect={true}
/>
<div className="text-sm text-gray-500">
Close on select enabled
</div>
</div>
</div>
{/* Limited Selection */}
<div className="bg-white rounded-xl shadow-lg p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4">Limited Selection</h2>
<div className="space-y-3">
<label className="block text-sm font-medium text-gray-700">
Select Up to 3 Fruits
</label>
<MultiSelect
options={fruitOptions}
value={limitedSelection}
onChange={setLimitedSelection}
maxSelectedItems={3}
placeholder="Maximum 3 selections..."
tagClassName="bg-purple-100 text-purple-800"
/>
<div className="text-sm text-gray-500">
{limitedSelection.length}/3 selected
</div>
</div>
</div>
{/* Loading State */}
<div className="bg-white rounded-xl shadow-lg p-6">
<h2 className="text-xl font-semibold text-gray-800 mb-4">Loading State</h2>
<div className="space-y-3">
<label className="block text-sm font-medium text-gray-700">
Demo Loading State
</label>
<MultiSelect
options={fruitOptions}
value={[]}
onChange={() => {}}
loading={loading}
loadingMessage="Fetching options..."
placeholder="Click button to see loading..."
/>
<button
onClick={handleLoadingDemo}
disabled={loading}
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 transition-colors text-sm"
>
{loading ? 'Loading...' : 'Trigger Loading'}
</button>
</div>
</div>
</div>
{/* Features Section */}
<div className="mt-12 bg-white rounded-xl shadow-lg p-8">
<h2 className="text-2xl font-bold text-gray-800 mb-6">Key Features</h2>
<div className="grid gap-6 md:grid-cols-2 lg:grid-cols-3">
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-8 h-8 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
</div>
<div>
<h3 className="font-semibold text-gray-800">Multi-Select with Tags</h3>
<p className="text-sm text-gray-600">Selected items appear as removable tags</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-8 h-8 bg-green-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
</div>
<div>
<h3 className="font-semibold text-gray-800">Search Functionality</h3>
<p className="text-sm text-gray-600">Filter options with real-time search</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-8 h-8 bg-purple-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 100 4m0-4v2m0-6V4" />
</svg>
</div>
<div>
<h3 className="font-semibold text-gray-800">Keyboard Navigation</h3>
<p className="text-sm text-gray-600">Full keyboard support with arrow keys</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-8 h-8 bg-yellow-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-yellow-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zM7 3H5a2 2 0 00-2 2v12a4 4 0 004 4h2" />
</svg>
</div>
<div>
<h3 className="font-semibold text-gray-800">Tailwind Styled</h3>
<p className="text-sm text-gray-600">Beautiful, customizable design</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-8 h-8 bg-red-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<div>
<h3 className="font-semibold text-gray-800">Accessible</h3>
<p className="text-sm text-gray-600">ARIA compliant and screen reader friendly</p>
</div>
</div>
<div className="flex items-start space-x-3">
<div className="flex-shrink-0 w-8 h-8 bg-indigo-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-indigo-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div>
<h3 className="font-semibold text-gray-800">TypeScript</h3>
<p className="text-sm text-gray-600">Full TypeScript support with proper types</p>
</div>
</div>
</div>
</div>
{/* Installation */}
<div className="mt-12 bg-gray-900 rounded-xl shadow-lg p-8 text-white">
<h2 className="text-2xl font-bold mb-4">Installation</h2>
<div className="bg-gray-800 rounded-lg p-4 font-mono text-sm">
<span className="text-green-400">npm install</span> react-multi-select-tabs
</div>
<p className="mt-4 text-gray-300">
Make sure you have Tailwind CSS installed and configured in your project.
</p>
</div>
</div>
</div>
);
};
export default Index;