react-multiselect-ui 2.0.0 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +690 -59
- package/package.json +3 -7
- package/dist/index.cjs +0 -46
- package/dist/index.cjs.map +0 -1
- package/dist/index.d.ts +0 -1
- package/dist/index.mjs +0 -5994
- package/dist/index.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,73 +1,704 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-multiselect-ui
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A ShadCN-style, fully accessible **Multi Select** component for React — built with Tailwind CSS, Radix UI, and cmdk.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/react-multiselect-ui)
|
|
6
|
+
[](https://www.npmjs.com/package/react-multiselect-ui)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://www.typescriptlang.org/)
|
|
6
9
|
|
|
7
|
-
|
|
8
|
-
- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/)
|
|
10
|
+
---
|
|
9
11
|
|
|
10
|
-
##
|
|
12
|
+
## Features
|
|
11
13
|
|
|
12
|
-
|
|
14
|
+
- ✅ Select multiple options with badge display
|
|
15
|
+
- ✅ Searchable dropdown powered by `cmdk`
|
|
16
|
+
- ✅ Keyboard navigation (Arrow keys, Enter, Escape)
|
|
17
|
+
- ✅ Full WAI-ARIA accessibility
|
|
18
|
+
- ✅ Dark mode support out of the box
|
|
19
|
+
- ✅ Max selection limit (`maxCount`)
|
|
20
|
+
- ✅ Clear all / remove individual selections
|
|
21
|
+
- ✅ TypeScript — fully typed props and exports
|
|
22
|
+
- ✅ ShadCN-compatible Tailwind styling
|
|
23
|
+
- ✅ No lucide-react dependency — uses inline SVGs
|
|
24
|
+
- ✅ Supports React 17, 18, and 19
|
|
25
|
+
- ✅ Supports Tailwind CSS v3 and v4
|
|
13
26
|
|
|
14
|
-
|
|
27
|
+
---
|
|
15
28
|
|
|
16
|
-
|
|
29
|
+
## Compatibility
|
|
17
30
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
31
|
+
| Tool | Supported Versions |
|
|
32
|
+
|---|---|
|
|
33
|
+
| React | 17, 18, 19 |
|
|
34
|
+
| Tailwind CSS | v3, v4 |
|
|
35
|
+
| TypeScript | 4.x, 5.x |
|
|
36
|
+
| Node.js | 18+ |
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
# npm
|
|
44
|
+
npm install react-multiselect-ui
|
|
45
|
+
|
|
46
|
+
# yarn
|
|
47
|
+
yarn add react-multiselect-ui
|
|
48
|
+
|
|
49
|
+
# pnpm
|
|
50
|
+
pnpm add react-multiselect-ui
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Peer Dependencies
|
|
56
|
+
|
|
57
|
+
Make sure these are already in your project:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
npm install react react-dom
|
|
44
61
|
```
|
|
45
62
|
|
|
46
|
-
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## Tailwind Setup
|
|
66
|
+
|
|
67
|
+
> This step is required for the component to be styled correctly. Skip it and the component will render unstyled.
|
|
68
|
+
|
|
69
|
+
### Tailwind v4
|
|
70
|
+
|
|
71
|
+
Add the `@source` directive so Tailwind scans the component's class names:
|
|
72
|
+
|
|
73
|
+
```css
|
|
74
|
+
/* src/index.css or src/globals.css */
|
|
75
|
+
@import "tailwindcss";
|
|
76
|
+
|
|
77
|
+
/* Required — tells Tailwind v4 to scan the package */
|
|
78
|
+
@source "../../node_modules/react-multiselect-ui/dist";
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
> Adjust the relative path based on where your CSS file lives:
|
|
82
|
+
> - CSS at `src/index.css` → use `../../node_modules/...`
|
|
83
|
+
> - CSS at root `styles/globals.css` → use `../node_modules/...`
|
|
84
|
+
> - CSS at root `index.css` → use `./node_modules/...`
|
|
85
|
+
|
|
86
|
+
### Tailwind v3
|
|
87
|
+
|
|
88
|
+
Add the dist path to the `content` array in `tailwind.config.js`:
|
|
47
89
|
|
|
48
90
|
```js
|
|
49
|
-
//
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
// Other configs...
|
|
59
|
-
// Enable lint rules for React
|
|
60
|
-
reactX.configs['recommended-typescript'],
|
|
61
|
-
// Enable lint rules for React DOM
|
|
62
|
-
reactDom.configs.recommended,
|
|
63
|
-
],
|
|
64
|
-
languageOptions: {
|
|
65
|
-
parserOptions: {
|
|
66
|
-
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
|
67
|
-
tsconfigRootDir: import.meta.dirname,
|
|
68
|
-
},
|
|
69
|
-
// other options...
|
|
70
|
-
},
|
|
91
|
+
// tailwind.config.js
|
|
92
|
+
module.exports = {
|
|
93
|
+
content: [
|
|
94
|
+
"./src/**/*.{js,ts,jsx,tsx}",
|
|
95
|
+
// Required — tells Tailwind v3 to scan the package
|
|
96
|
+
"./node_modules/react-multiselect-ui/dist/**/*.{js,mjs}",
|
|
97
|
+
],
|
|
98
|
+
theme: {
|
|
99
|
+
extend: {},
|
|
71
100
|
},
|
|
72
|
-
]
|
|
101
|
+
plugins: [],
|
|
102
|
+
};
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Quick Start
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { useState } from "react";
|
|
111
|
+
import { MultiSelect } from "react-multiselect-ui";
|
|
112
|
+
|
|
113
|
+
const options = [
|
|
114
|
+
{ value: "react", label: "React" },
|
|
115
|
+
{ value: "vue", label: "Vue" },
|
|
116
|
+
{ value: "svelte", label: "Svelte" },
|
|
117
|
+
{ value: "angular", label: "Angular" },
|
|
118
|
+
];
|
|
119
|
+
|
|
120
|
+
export default function App() {
|
|
121
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
122
|
+
|
|
123
|
+
return (
|
|
124
|
+
<MultiSelect
|
|
125
|
+
options={options}
|
|
126
|
+
value={selected}
|
|
127
|
+
onValueChange={setSelected}
|
|
128
|
+
placeholder="Select frameworks..."
|
|
129
|
+
/>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
136
|
+
## Examples
|
|
137
|
+
|
|
138
|
+
### 1. Basic Usage
|
|
139
|
+
|
|
140
|
+
```tsx
|
|
141
|
+
import { useState } from "react";
|
|
142
|
+
import { MultiSelect } from "react-multiselect-ui";
|
|
143
|
+
|
|
144
|
+
const FRUITS = [
|
|
145
|
+
{ value: "apple", label: "Apple" },
|
|
146
|
+
{ value: "banana", label: "Banana" },
|
|
147
|
+
{ value: "cherry", label: "Cherry" },
|
|
148
|
+
{ value: "mango", label: "Mango" },
|
|
149
|
+
{ value: "orange", label: "Orange" },
|
|
150
|
+
];
|
|
151
|
+
|
|
152
|
+
export function BasicExample() {
|
|
153
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<div className="w-80">
|
|
157
|
+
<MultiSelect
|
|
158
|
+
options={FRUITS}
|
|
159
|
+
value={selected}
|
|
160
|
+
onValueChange={setSelected}
|
|
161
|
+
placeholder="Pick some fruits..."
|
|
162
|
+
/>
|
|
163
|
+
<p className="mt-2 text-sm text-gray-500">
|
|
164
|
+
Selected: {selected.length === 0 ? "none" : selected.join(", ")}
|
|
165
|
+
</p>
|
|
166
|
+
</div>
|
|
167
|
+
);
|
|
168
|
+
}
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
### 2. Pre-selected Values
|
|
174
|
+
|
|
175
|
+
Initialize `useState` with values to show defaults on first render:
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
export function PreselectedExample() {
|
|
179
|
+
const [languages, setLanguages] = useState<string[]>(["en", "es"]);
|
|
180
|
+
|
|
181
|
+
return (
|
|
182
|
+
<MultiSelect
|
|
183
|
+
options={[
|
|
184
|
+
{ value: "en", label: "English" },
|
|
185
|
+
{ value: "es", label: "Spanish" },
|
|
186
|
+
{ value: "fr", label: "French" },
|
|
187
|
+
{ value: "de", label: "German" },
|
|
188
|
+
{ value: "zh", label: "Chinese" },
|
|
189
|
+
]}
|
|
190
|
+
value={languages}
|
|
191
|
+
onValueChange={setLanguages}
|
|
192
|
+
placeholder="Select languages..."
|
|
193
|
+
/>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
---
|
|
199
|
+
|
|
200
|
+
### 3. Max Selection Limit
|
|
201
|
+
|
|
202
|
+
Use `maxCount` to cap how many items can be selected. Options automatically disable once the limit is reached:
|
|
203
|
+
|
|
204
|
+
```tsx
|
|
205
|
+
export function MaxSelectExample() {
|
|
206
|
+
const [toppings, setToppings] = useState<string[]>([]);
|
|
207
|
+
|
|
208
|
+
return (
|
|
209
|
+
<div className="w-80">
|
|
210
|
+
<MultiSelect
|
|
211
|
+
options={[
|
|
212
|
+
{ value: "cheese", label: "Extra Cheese" },
|
|
213
|
+
{ value: "mushrooms", label: "Mushrooms" },
|
|
214
|
+
{ value: "peppers", label: "Bell Peppers" },
|
|
215
|
+
{ value: "onions", label: "Onions" },
|
|
216
|
+
{ value: "olives", label: "Olives" },
|
|
217
|
+
]}
|
|
218
|
+
value={toppings}
|
|
219
|
+
onValueChange={setToppings}
|
|
220
|
+
placeholder="Select toppings..."
|
|
221
|
+
maxCount={3}
|
|
222
|
+
/>
|
|
223
|
+
{toppings.length === 3 && (
|
|
224
|
+
<p className="mt-1 text-xs text-amber-600">
|
|
225
|
+
Maximum 3 toppings reached!
|
|
226
|
+
</p>
|
|
227
|
+
)}
|
|
228
|
+
</div>
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
### 4. Disabled Component
|
|
236
|
+
|
|
237
|
+
Disable the entire component conditionally:
|
|
238
|
+
|
|
239
|
+
```tsx
|
|
240
|
+
export function DisabledExample() {
|
|
241
|
+
const [isLocked, setIsLocked] = useState(true);
|
|
242
|
+
const [roles, setRoles] = useState<string[]>(["viewer"]);
|
|
243
|
+
|
|
244
|
+
return (
|
|
245
|
+
<div className="space-y-2 w-80">
|
|
246
|
+
<MultiSelect
|
|
247
|
+
options={[
|
|
248
|
+
{ value: "admin", label: "Admin" },
|
|
249
|
+
{ value: "editor", label: "Editor" },
|
|
250
|
+
{ value: "viewer", label: "Viewer" },
|
|
251
|
+
{ value: "guest", label: "Guest" },
|
|
252
|
+
]}
|
|
253
|
+
value={roles}
|
|
254
|
+
onValueChange={setRoles}
|
|
255
|
+
placeholder="Select roles..."
|
|
256
|
+
disabled={isLocked}
|
|
257
|
+
/>
|
|
258
|
+
<button
|
|
259
|
+
onClick={() => setIsLocked((v) => !v)}
|
|
260
|
+
className="text-sm text-blue-600 underline"
|
|
261
|
+
>
|
|
262
|
+
{isLocked ? "Unlock" : "Lock"} selector
|
|
263
|
+
</button>
|
|
264
|
+
</div>
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
---
|
|
270
|
+
|
|
271
|
+
### 5. Disabled Individual Options
|
|
272
|
+
|
|
273
|
+
Mark specific options as unselectable while keeping others available:
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
const PLANS = [
|
|
277
|
+
{ value: "free", label: "Free Tier" },
|
|
278
|
+
{ value: "pro", label: "Pro" },
|
|
279
|
+
{ value: "team", label: "Team" },
|
|
280
|
+
// This option cannot be selected
|
|
281
|
+
{ value: "enterprise", label: "Enterprise — contact sales", disabled: true },
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
export function DisabledOptionsExample() {
|
|
285
|
+
const [plan, setPlan] = useState<string[]>([]);
|
|
286
|
+
|
|
287
|
+
return (
|
|
288
|
+
<MultiSelect
|
|
289
|
+
options={PLANS}
|
|
290
|
+
value={plan}
|
|
291
|
+
onValueChange={setPlan}
|
|
292
|
+
placeholder="Select a plan..."
|
|
293
|
+
/>
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
---
|
|
299
|
+
|
|
300
|
+
### 6. Inside a Form with Reset
|
|
301
|
+
|
|
302
|
+
```tsx
|
|
303
|
+
export function FormExample() {
|
|
304
|
+
const [skills, setSkills] = useState<string[]>([]);
|
|
305
|
+
const [submitted, setSubmitted] = useState<string[]>([]);
|
|
306
|
+
|
|
307
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
308
|
+
e.preventDefault();
|
|
309
|
+
setSubmitted([...skills]);
|
|
310
|
+
setSkills([]); // Reset after submit
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
return (
|
|
314
|
+
<form onSubmit={handleSubmit} className="space-y-3 w-80">
|
|
315
|
+
<MultiSelect
|
|
316
|
+
options={[
|
|
317
|
+
{ value: "typescript", label: "TypeScript" },
|
|
318
|
+
{ value: "react", label: "React" },
|
|
319
|
+
{ value: "node", label: "Node.js" },
|
|
320
|
+
{ value: "python", label: "Python" },
|
|
321
|
+
{ value: "go", label: "Go" },
|
|
322
|
+
{ value: "rust", label: "Rust" },
|
|
323
|
+
]}
|
|
324
|
+
value={skills}
|
|
325
|
+
onValueChange={setSkills}
|
|
326
|
+
placeholder="Select your skills..."
|
|
327
|
+
/>
|
|
328
|
+
<button
|
|
329
|
+
type="submit"
|
|
330
|
+
disabled={skills.length === 0}
|
|
331
|
+
className="w-full rounded-md bg-zinc-900 px-4 py-2 text-sm
|
|
332
|
+
font-medium text-white disabled:opacity-50"
|
|
333
|
+
>
|
|
334
|
+
Submit
|
|
335
|
+
</button>
|
|
336
|
+
{submitted.length > 0 && (
|
|
337
|
+
<p className="text-sm text-green-700">
|
|
338
|
+
Submitted: {submitted.join(", ")}
|
|
339
|
+
</p>
|
|
340
|
+
)}
|
|
341
|
+
</form>
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
---
|
|
347
|
+
|
|
348
|
+
### 7. Without Clear Button
|
|
349
|
+
|
|
350
|
+
Hide the "Clear all" control so users can only deselect via individual badge X buttons:
|
|
351
|
+
|
|
352
|
+
```tsx
|
|
353
|
+
<MultiSelect
|
|
354
|
+
options={options}
|
|
355
|
+
value={selected}
|
|
356
|
+
onValueChange={setSelected}
|
|
357
|
+
placeholder="Add labels..."
|
|
358
|
+
clearable={false}
|
|
359
|
+
/>
|
|
360
|
+
```
|
|
361
|
+
|
|
362
|
+
---
|
|
363
|
+
|
|
364
|
+
### 8. Custom Search and Empty Text
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
<MultiSelect
|
|
368
|
+
options={options}
|
|
369
|
+
value={selected}
|
|
370
|
+
onValueChange={setSelected}
|
|
371
|
+
placeholder="Choose team members..."
|
|
372
|
+
searchPlaceholder="Type a name to search..."
|
|
373
|
+
emptyText="No team members match your search."
|
|
374
|
+
/>
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
|
|
379
|
+
### 9. Custom Styling
|
|
380
|
+
|
|
381
|
+
Use `className` to style the trigger button and `contentClassName` for the dropdown panel:
|
|
382
|
+
|
|
383
|
+
```tsx
|
|
384
|
+
<MultiSelect
|
|
385
|
+
options={options}
|
|
386
|
+
value={selected}
|
|
387
|
+
onValueChange={setSelected}
|
|
388
|
+
placeholder="Pick colors..."
|
|
389
|
+
className="border-violet-300 focus-visible:ring-violet-500"
|
|
390
|
+
contentClassName="border-violet-200"
|
|
391
|
+
/>
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
### 10. Async / API-loaded Options
|
|
397
|
+
|
|
398
|
+
```tsx
|
|
399
|
+
import { useState, useEffect } from "react";
|
|
400
|
+
import { MultiSelect } from "react-multiselect-ui";
|
|
401
|
+
import type { MultiSelectOption } from "react-multiselect-ui";
|
|
402
|
+
|
|
403
|
+
export function AsyncExample() {
|
|
404
|
+
const [options, setOptions] = useState<MultiSelectOption[]>([]);
|
|
405
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
406
|
+
const [loading, setLoading] = useState(true);
|
|
407
|
+
|
|
408
|
+
useEffect(() => {
|
|
409
|
+
fetch("/api/users")
|
|
410
|
+
.then((res) => res.json())
|
|
411
|
+
.then((data) => {
|
|
412
|
+
setOptions(
|
|
413
|
+
data.map((u: { id: string; name: string }) => ({
|
|
414
|
+
value: u.id,
|
|
415
|
+
label: u.name,
|
|
416
|
+
}))
|
|
417
|
+
);
|
|
418
|
+
setLoading(false);
|
|
419
|
+
});
|
|
420
|
+
}, []);
|
|
421
|
+
|
|
422
|
+
if (loading) {
|
|
423
|
+
return (
|
|
424
|
+
<div className="h-9 w-full animate-pulse rounded-md bg-zinc-100" />
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
return (
|
|
429
|
+
<MultiSelect
|
|
430
|
+
options={options}
|
|
431
|
+
value={selected}
|
|
432
|
+
onValueChange={setSelected}
|
|
433
|
+
placeholder="Assign team members..."
|
|
434
|
+
searchPlaceholder="Search by name..."
|
|
435
|
+
/>
|
|
436
|
+
);
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
---
|
|
441
|
+
|
|
442
|
+
### 11. Accessible with ARIA Label
|
|
443
|
+
|
|
444
|
+
```tsx
|
|
445
|
+
<MultiSelect
|
|
446
|
+
options={options}
|
|
447
|
+
value={selected}
|
|
448
|
+
onValueChange={setSelected}
|
|
449
|
+
placeholder="Filter by department..."
|
|
450
|
+
aria-label="Filter results by department"
|
|
451
|
+
/>
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
### 12. Controlled Reset from Parent
|
|
457
|
+
|
|
458
|
+
```tsx
|
|
459
|
+
export function ParentControlExample() {
|
|
460
|
+
const [selected, setSelected] = useState<string[]>(["react", "typescript"]);
|
|
461
|
+
|
|
462
|
+
return (
|
|
463
|
+
<div className="space-y-3 w-80">
|
|
464
|
+
<MultiSelect
|
|
465
|
+
options={options}
|
|
466
|
+
value={selected}
|
|
467
|
+
onValueChange={setSelected}
|
|
468
|
+
placeholder="Select technologies..."
|
|
469
|
+
/>
|
|
470
|
+
<div className="flex gap-2">
|
|
471
|
+
<button onClick={() => setSelected([])}>
|
|
472
|
+
Clear All
|
|
473
|
+
</button>
|
|
474
|
+
<button onClick={() => setSelected(["react", "typescript", "node"])}>
|
|
475
|
+
Reset to Defaults
|
|
476
|
+
</button>
|
|
477
|
+
</div>
|
|
478
|
+
</div>
|
|
479
|
+
);
|
|
480
|
+
}
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
---
|
|
484
|
+
|
|
485
|
+
## API Reference
|
|
486
|
+
|
|
487
|
+
### `MultiSelectOption`
|
|
488
|
+
|
|
489
|
+
```ts
|
|
490
|
+
interface MultiSelectOption {
|
|
491
|
+
value: string; // Unique identifier stored in the value array
|
|
492
|
+
label: string; // Display text shown in the dropdown and badges
|
|
493
|
+
disabled?: boolean; // Prevents this option from being selected
|
|
494
|
+
}
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
### `MultiSelectProps`
|
|
498
|
+
|
|
499
|
+
| Prop | Type | Default | Description |
|
|
500
|
+
|---|---|---|---|
|
|
501
|
+
| `options` | `MultiSelectOption[]` | required | Array of selectable options |
|
|
502
|
+
| `value` | `string[]` | `[]` | Currently selected values (controlled) |
|
|
503
|
+
| `onValueChange` | `(value: string[]) => void` | — | Called when selection changes |
|
|
504
|
+
| `placeholder` | `string` | `"Select options..."` | Shown when nothing is selected |
|
|
505
|
+
| `searchPlaceholder` | `string` | `"Search..."` | Placeholder inside the search input |
|
|
506
|
+
| `emptyText` | `string` | `"No options found."` | Shown when search has no results |
|
|
507
|
+
| `disabled` | `boolean` | `false` | Disables the entire component |
|
|
508
|
+
| `maxCount` | `number` | — | Maximum number of selectable items |
|
|
509
|
+
| `clearable` | `boolean` | `true` | Show / hide the clear all control |
|
|
510
|
+
| `className` | `string` | — | Extra classes for the trigger button |
|
|
511
|
+
| `contentClassName` | `string` | — | Extra classes for the dropdown panel |
|
|
512
|
+
| `aria-label` | `string` | — | Accessible label for screen readers |
|
|
513
|
+
|
|
514
|
+
---
|
|
515
|
+
|
|
516
|
+
## Keyboard Navigation
|
|
517
|
+
|
|
518
|
+
| Key | Action |
|
|
519
|
+
|---|---|
|
|
520
|
+
| `Space` / `Enter` | Open dropdown / select focused option |
|
|
521
|
+
| `↑` / `↓` | Navigate through options |
|
|
522
|
+
| `Escape` | Close the dropdown |
|
|
523
|
+
| `Tab` | Move focus out of the dropdown |
|
|
524
|
+
|
|
525
|
+
---
|
|
526
|
+
|
|
527
|
+
## TypeScript
|
|
528
|
+
|
|
529
|
+
All types are exported from the package root:
|
|
530
|
+
|
|
531
|
+
```ts
|
|
532
|
+
import { MultiSelect } from "react-multiselect-ui";
|
|
533
|
+
import type { MultiSelectProps, MultiSelectOption } from "react-multiselect-ui";
|
|
534
|
+
|
|
535
|
+
// Build options dynamically with full type safety
|
|
536
|
+
const buildOptions = (
|
|
537
|
+
items: { id: string; name: string }[]
|
|
538
|
+
): MultiSelectOption[] =>
|
|
539
|
+
items.map((item) => ({ value: item.id, label: item.name }));
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
---
|
|
543
|
+
|
|
544
|
+
## Framework Guides
|
|
545
|
+
|
|
546
|
+
### Next.js App Router
|
|
547
|
+
|
|
548
|
+
Add `"use client"` at the top of any file that uses the component since it uses React state:
|
|
549
|
+
|
|
550
|
+
```tsx
|
|
551
|
+
"use client";
|
|
552
|
+
|
|
553
|
+
import { useState } from "react";
|
|
554
|
+
import { MultiSelect } from "react-multiselect-ui";
|
|
555
|
+
|
|
556
|
+
export default function Page() {
|
|
557
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
558
|
+
|
|
559
|
+
return (
|
|
560
|
+
<MultiSelect
|
|
561
|
+
options={options}
|
|
562
|
+
value={selected}
|
|
563
|
+
onValueChange={setSelected}
|
|
564
|
+
/>
|
|
565
|
+
);
|
|
566
|
+
}
|
|
73
567
|
```
|
|
568
|
+
|
|
569
|
+
### Next.js Pages Router
|
|
570
|
+
|
|
571
|
+
Works without any extra configuration:
|
|
572
|
+
|
|
573
|
+
```tsx
|
|
574
|
+
import { useState } from "react";
|
|
575
|
+
import { MultiSelect } from "react-multiselect-ui";
|
|
576
|
+
|
|
577
|
+
export default function Page() {
|
|
578
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
579
|
+
|
|
580
|
+
return (
|
|
581
|
+
<MultiSelect
|
|
582
|
+
options={options}
|
|
583
|
+
value={selected}
|
|
584
|
+
onValueChange={setSelected}
|
|
585
|
+
/>
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
### Vite + React
|
|
591
|
+
|
|
592
|
+
Works out of the box. Just follow the Tailwind setup above.
|
|
593
|
+
|
|
594
|
+
### Remix
|
|
595
|
+
|
|
596
|
+
```tsx
|
|
597
|
+
// app/routes/_index.tsx
|
|
598
|
+
import { useState } from "react";
|
|
599
|
+
import { MultiSelect } from "react-multiselect-ui";
|
|
600
|
+
|
|
601
|
+
export default function Index() {
|
|
602
|
+
const [selected, setSelected] = useState<string[]>([]);
|
|
603
|
+
|
|
604
|
+
return (
|
|
605
|
+
<MultiSelect
|
|
606
|
+
options={options}
|
|
607
|
+
value={selected}
|
|
608
|
+
onValueChange={setSelected}
|
|
609
|
+
/>
|
|
610
|
+
);
|
|
611
|
+
}
|
|
612
|
+
```
|
|
613
|
+
|
|
614
|
+
---
|
|
615
|
+
|
|
616
|
+
## Troubleshooting
|
|
617
|
+
|
|
618
|
+
### Component renders with no styles
|
|
619
|
+
|
|
620
|
+
You are missing the Tailwind scan configuration. See the **Tailwind Setup** section above for your version (v3 or v4).
|
|
621
|
+
|
|
622
|
+
### Dropdown appears behind modals or sticky headers
|
|
623
|
+
|
|
624
|
+
The dropdown uses a Radix UI Portal (mounts directly on `<body>`). Override the z-index via `contentClassName`:
|
|
625
|
+
|
|
626
|
+
```tsx
|
|
627
|
+
<MultiSelect contentClassName="z-[9999]" ... />
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
### TypeScript error: Cannot find module
|
|
631
|
+
|
|
632
|
+
Make sure you import from the package root:
|
|
633
|
+
|
|
634
|
+
```ts
|
|
635
|
+
// ✅ Correct
|
|
636
|
+
import { MultiSelect } from "react-multiselect-ui";
|
|
637
|
+
import type { MultiSelectOption } from "react-multiselect-ui";
|
|
638
|
+
|
|
639
|
+
// ❌ Wrong — never import from internal paths
|
|
640
|
+
import { MultiSelect } from "react-multiselect-ui/dist/index.mjs";
|
|
641
|
+
```
|
|
642
|
+
|
|
643
|
+
### React 17 — JSX transform error
|
|
644
|
+
|
|
645
|
+
Make sure your tsconfig has the new JSX transform:
|
|
646
|
+
|
|
647
|
+
```json
|
|
648
|
+
{
|
|
649
|
+
"compilerOptions": {
|
|
650
|
+
"jsx": "react-jsx"
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
```
|
|
654
|
+
|
|
655
|
+
---
|
|
656
|
+
|
|
657
|
+
## Migrating from `@parag.vora/react-multiselect-ui`
|
|
658
|
+
|
|
659
|
+
```bash
|
|
660
|
+
# Remove old package
|
|
661
|
+
npm uninstall @parag.vora/react-multiselect-ui
|
|
662
|
+
|
|
663
|
+
# Install new package
|
|
664
|
+
npm install react-multiselect-ui
|
|
665
|
+
```
|
|
666
|
+
|
|
667
|
+
Update your imports:
|
|
668
|
+
|
|
669
|
+
```ts
|
|
670
|
+
// Before
|
|
671
|
+
import { MultiSelect } from "@parag.vora/react-multiselect-ui";
|
|
672
|
+
|
|
673
|
+
// After
|
|
674
|
+
import { MultiSelect } from "react-multiselect-ui";
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
The API is identical — no other changes needed.
|
|
678
|
+
|
|
679
|
+
---
|
|
680
|
+
|
|
681
|
+
## Contributing
|
|
682
|
+
|
|
683
|
+
Contributions, issues, and feature requests are welcome!
|
|
684
|
+
|
|
685
|
+
1. Fork the repository
|
|
686
|
+
2. Create your branch: `git checkout -b feat/my-feature`
|
|
687
|
+
3. Commit: `git commit -m "feat: add my feature"`
|
|
688
|
+
4. Push: `git push origin feat/my-feature`
|
|
689
|
+
5. Open a Pull Request on GitHub
|
|
690
|
+
|
|
691
|
+
---
|
|
692
|
+
|
|
693
|
+
## License
|
|
694
|
+
|
|
695
|
+
MIT © [Parag Vora](mailto:paragvora19@gmail.com)
|
|
696
|
+
|
|
697
|
+
---
|
|
698
|
+
|
|
699
|
+
## Links
|
|
700
|
+
|
|
701
|
+
- [npm](https://www.npmjs.com/package/react-multiselect-ui)
|
|
702
|
+
- [GitHub](https://github.com/paragvora23/react-multiselect-ui)
|
|
703
|
+
- [Live Demo](https://react-multiselect-ui.netlify.app)
|
|
704
|
+
- [Report an Issue](https://github.com/paragvora23/react-multiselect-ui/issues)
|