npm package: @parag.vora/react-multiselect-ui
Description: A ShadCN-style accessible Multi Select component built with React, TypeScript, Tailwind CSS, Radix UI, and cmdk
Author: Parag Vora
Latest version: 1.0.2
Deprecated: Package moved to react-multiselect-ui — please update your dependency

# @parag.vora/react-multiselect-ui

A ShadCN-style, fully accessible **Multi Select** component for React 19+ — built with Tailwind CSS v4, Radix UI, and cmdk.

[![npm version](https://img.shields.io/npm/v/@parag.vora/react-multiselect-ui)](https://www.npmjs.com/package/@parag.vora/react-multiselect-ui)
[![npm downloads](https://img.shields.io/npm/dm/@parag.vora/react-multiselect-ui)](https://www.npmjs.com/package/@parag.vora/react-multiselect-ui)
[![license](https://img.shields.io/npm/l/@parag.vora/react-multiselect-ui)](./LICENSE)
[![TypeScript](https://img.shields.io/badge/TypeScript-ready-blue)](https://www.typescriptlang.org/)

---

## Features

- ✅ Select multiple options with badge display
- ✅ Searchable dropdown powered by `cmdk`
- ✅ Keyboard navigation (Arrow keys, Enter, Escape)
- ✅ Full WAI-ARIA accessibility
- ✅ Dark mode support out of the box
- ✅ Max selection limit (`maxCount`)
- ✅ Custom icons per option
- ✅ Clear all / remove individual selections
- ✅ TypeScript — fully typed props and exports
- ✅ ShadCN-compatible Tailwind styling

---

## Requirements & Peer Dependencies

> ⚠️ **Read this before installing.** This library has peer dependencies that **must already be present** in your project. Installing without them will cause runtime errors.

| Peer Dependency | Required Version | Why |
|---|---|---|
| `react` | `^19.0.0` | Core framework |
| `react-dom` | `^19.0.0` | DOM rendering |
| `tailwindcss` | `^4.0.0` | Styling (must scan component files) |

This package bundles its own copies of `@radix-ui/react-popover`, `cmdk`, `lucide-react`, `clsx`, and `tailwind-merge` — you do **not** need to install these separately.

### Tailwind CSS v4 — Important Setup Note

Unlike Tailwind v3, **Tailwind v4 does not use a `tailwind.config.js`**. Instead, it auto-detects class usage by scanning files. For it to scan this library's component classes, you must configure your CSS source:

```css
/* your project's main CSS file (e.g. src/index.css or src/globals.css) */
@import "tailwindcss";

/* Tell Tailwind to scan node_modules for this package */
@source "../../node_modules/@parag.vora/react-multiselect-ui/dist";
```

> Without the `@source` directive, Tailwind v4 will **not** include the component's utility classes in your output CSS, and the component will appear unstyled.

---

## Installation

```bash
# npm
npm install @parag.vora/react-multiselect-ui

# yarn
yarn add @parag.vora/react-multiselect-ui

# pnpm
pnpm add @parag.vora/react-multiselect-ui
```

If you don't have the peer dependencies yet:

```bash
npm install react@^19 react-dom@^19 tailwindcss@^4
```

---

## Quick Start

```tsx
import { useState } from "react";
import { MultiSelect } from "@parag.vora/react-multiselect-ui";

const options = [
  { value: "react", label: "React" },
  { value: "vue", label: "Vue" },
  { value: "svelte", label: "Svelte" },
];

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

  return (
    <MultiSelect
      options={options}
      value={selected}
      onValueChange={setSelected}
      placeholder="Select frameworks..."
    />
  );
}
```

---

## Examples

### 1. Basic Multi Select

The simplest usage — just pass options, value, and a change handler.

```tsx
import { useState } from "react";
import { MultiSelect } from "@parag.vora/react-multiselect-ui";

const FRUITS = [
  { value: "apple", label: "Apple" },
  { value: "banana", label: "Banana" },
  { value: "cherry", label: "Cherry" },
  { value: "mango", label: "Mango" },
  { value: "orange", label: "Orange" },
];

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

  return (
    <div className="w-80">
      <MultiSelect
        options={FRUITS}
        value={selected}
        onValueChange={setSelected}
        placeholder="Pick some fruits..."
      />
      <p className="mt-2 text-sm text-gray-500">
        Selected: {selected.length === 0 ? "none" : selected.join(", ")}
      </p>
    </div>
  );
}
```

---

### 2. With Icons Per Option

Pass any React node as an `icon` to each option — great for flags, logos, or lucide icons.

```tsx
import { useState } from "react";
import { MultiSelect } from "@parag.vora/react-multiselect-ui";
import { Globe, Zap, Shield, Database, Cloud, Cpu } from "lucide-react";

const TECH_STACK = [
  { value: "nextjs",    label: "Next.js",    icon: <Globe className="h-4 w-4" /> },
  { value: "remix",     label: "Remix",      icon: <Zap className="h-4 w-4" /> },
  { value: "astro",     label: "Astro",      icon: <Shield className="h-4 w-4" /> },
  { value: "nuxt",      label: "Nuxt",       icon: <Database className="h-4 w-4" /> },
  { value: "sveltekit", label: "SvelteKit",  icon: <Cloud className="h-4 w-4" /> },
  { value: "qwik",      label: "Qwik",       icon: <Cpu className="h-4 w-4" /> },
];

export function WithIconsExample() {
  const [selected, setSelected] = useState<string[]>(["nextjs"]);

  return (
    <MultiSelect
      options={TECH_STACK}
      value={selected}
      onValueChange={setSelected}
      placeholder="Choose your stack..."
    />
  );
}
```

---

### 3. With a Maximum Selection Limit

Use `maxCount` to cap how many items can be selected. Options become disabled once the limit is reached.

```tsx
import { useState } from "react";
import { MultiSelect } from "@parag.vora/react-multiselect-ui";

const TOPPINGS = [
  { value: "cheese",    label: "Extra Cheese" },
  { value: "mushrooms", label: "Mushrooms" },
  { value: "peppers",   label: "Bell Peppers" },
  { value: "onions",    label: "Onions" },
  { value: "olives",    label: "Olives" },
  { value: "jalapenos", label: "Jalapeños" },
];

export function MaxSelectExample() {
  const [toppings, setToppings] = useState<string[]>([]);

  return (
    <div className="w-80">
      <label className="block text-sm font-medium text-gray-700 mb-1">
        Choose up to 3 toppings
      </label>
      <MultiSelect
        options={TOPPINGS}
        value={toppings}
        onValueChange={setToppings}
        placeholder="Select toppings..."
        maxCount={3}
      />
      {toppings.length === 3 && (
        <p className="mt-1 text-xs text-amber-600">Maximum toppings reached!</p>
      )}
    </div>
  );
}
```

---

### 4. Pre-selected Values (Default Selection)

Initialize `useState` with pre-selected values to show defaults on first render.

```tsx
import { useState } from "react";
import { MultiSelect } from "@parag.vora/react-multiselect-ui";

const LANGUAGES = [
  { value: "en", label: "English" },
  { value: "es", label: "Spanish" },
  { value: "fr", label: "French" },
  { value: "de", label: "German" },
  { value: "zh", label: "Chinese" },
  { value: "ja", label: "Japanese" },
  { value: "ar", label: "Arabic" },
];

export function PreselectedExample() {
  // English and Spanish pre-selected by default
  const [languages, setLanguages] = useState<string[]>(["en", "es"]);

  return (
    <MultiSelect
      options={LANGUAGES}
      value={languages}
      onValueChange={setLanguages}
      placeholder="Select languages..."
    />
  );
}
```

---

### 5. Disabled State

Disable the entire component by passing `disabled`. Useful when a form section is conditionally locked.

```tsx
import { useState } from "react";
import { MultiSelect } from "@parag.vora/react-multiselect-ui";

const ROLES = [
  { value: "admin",  label: "Admin" },
  { value: "editor", label: "Editor" },
  { value: "viewer", label: "Viewer" },
];

export function DisabledExample() {
  const [isLocked, setIsLocked] = useState(true);
  const [roles, setRoles] = useState<string[]>(["viewer"]);

  return (
    <div className="space-y-2">
      <MultiSelect
        options={ROLES}
        value={roles}
        onValueChange={setRoles}
        placeholder="Select roles..."
        disabled={isLocked}
      />
      <button
        onClick={() => setIsLocked((v) => !v)}
        className="text-sm text-blue-600 underline"
      >
        {isLocked ? "Unlock" : "Lock"} selector
      </button>
    </div>
  );
}
```

---

### 6. Disabled Individual Options

Mark specific options as `disabled` to make them unselectable while keeping others available.

```tsx
import { useState } from "react";
import { MultiSelect } from "@parag.vora/react-multiselect-ui";

const PLANS = [
  { value: "free",       label: "Free Tier" },
  { value: "pro",        label: "Pro" },
  { value: "enterprise", label: "Enterprise (contact sales)", disabled: true },
  { value: "custom",     label: "Custom (not available)",     disabled: true },
];

export function DisabledOptionsExample() {
  const [plan, setPlan] = useState<string[]>([]);

  return (
    <MultiSelect
      options={PLANS}
      value={plan}
      onValueChange={setPlan}
      placeholder="Select a plan..."
    />
  );
}
```

---

### 7. Inside a Form with Reset

Integrating with form state — includes a submit handler and a reset button.

```tsx
import { useState } from "react";
import { MultiSelect } from "@parag.vora/react-multiselect-ui";

const SKILLS = [
  { value: "typescript", label: "TypeScript" },
  { value: "react",      label: "React" },
  { value: "node",       label: "Node.js" },
  { value: "python",     label: "Python" },
  { value: "go",         label: "Go" },
  { value: "rust",       label: "Rust" },
];

export function FormExample() {
  const [skills, setSkills] = useState<string[]>([]);
  const [submitted, setSubmitted] = useState<string[]>([]);

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    setSubmitted(skills);
    setSkills([]); // Reset after submit
  };

  return (
    <form onSubmit={handleSubmit} className="space-y-3 w-80">
      <div>
        <label className="block text-sm font-medium text-gray-700 mb-1">
          Your Skills
        </label>
        <MultiSelect
          options={SKILLS}
          value={skills}
          onValueChange={setSkills}
          placeholder="Select skills..."
        />
      </div>

      <button
        type="submit"
        disabled={skills.length === 0}
        className="w-full rounded-md bg-zinc-900 px-4 py-2 text-sm
                   font-medium text-white disabled:opacity-50"
      >
        Submit
      </button>

      {submitted.length > 0 && (
        <p className="text-sm text-green-700">
          Submitted: {submitted.join(", ")}
        </p>
      )}
    </form>
  );
}
```

---

### 8. Custom Styling

Use `className` for the trigger and `contentClassName` for the dropdown panel.

```tsx
import { useState } from "react";
import { MultiSelect } from "@parag.vora/react-multiselect-ui";

const COLORS = [
  { value: "red",    label: "Red" },
  { value: "green",  label: "Green" },
  { value: "blue",   label: "Blue" },
  { value: "purple", label: "Purple" },
  { value: "yellow", label: "Yellow" },
];

export function CustomStylingExample() {
  const [colors, setColors] = useState<string[]>([]);

  return (
    <MultiSelect
      options={COLORS}
      value={colors}
      onValueChange={setColors}
      placeholder="Pick colors..."
      // Customize the trigger button
      className="border-2 border-violet-300 focus:ring-violet-500 rounded-xl"
      // Customize the dropdown panel
      contentClassName="border-violet-200 shadow-violet-100 shadow-lg"
    />
  );
}
```

---

### 9. Without the Clear Button

Hide the "Clear all" control when you want explicit-only deselection via individual badge X buttons.

```tsx
import { useState } from "react";
import { MultiSelect } from "@parag.vora/react-multiselect-ui";

const OPTIONS = [
  { value: "a", label: "Option A" },
  { value: "b", label: "Option B" },
  { value: "c", label: "Option C" },
];

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

  return (
    <MultiSelect
      options={OPTIONS}
      value={selected}
      onValueChange={setSelected}
      placeholder="Select options..."
      clearable={false}  // Hides the clear-all X button and footer link
    />
  );
}
```

---

### 10. Dynamic Options (Loaded from API)

Async options loaded from an API, showing a loading state while fetching.

```tsx
import { useState, useEffect } from "react";
import { MultiSelect, MultiSelectOption } from "@parag.vora/react-multiselect-ui";

export function AsyncOptionsExample() {
  const [options, setOptions] = useState<MultiSelectOption[]>([]);
  const [selected, setSelected] = useState<string[]>([]);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // Simulate an API call
    setTimeout(() => {
      setOptions([
        { value: "user_1", label: "Alice Johnson" },
        { value: "user_2", label: "Bob Smith" },
        { value: "user_3", label: "Carol White" },
        { value: "user_4", label: "David Lee" },
      ]);
      setLoading(false);
    }, 1000);
  }, []);

  if (loading) {
    return (
      <div className="h-9 w-full animate-pulse rounded-md bg-zinc-100" />
    );
  }

  return (
    <MultiSelect
      options={options}
      value={selected}
      onValueChange={setSelected}
      placeholder="Assign team members..."
      searchPlaceholder="Search by name..."
    />
  );
}
```

---

## API Reference

### `MultiSelectOption`

```ts
interface MultiSelectOption {
  value: string;          // Unique identifier stored in value array
  label: string;          // Display text shown in dropdown and badges
  disabled?: boolean;     // Prevents selecting this option
  icon?: React.ReactNode; // Optional icon rendered before the label
}
```

### `MultiSelectProps`

| Prop | Type | Default | Description |
|---|---|---|---|
| `options` | `MultiSelectOption[]` | required | Array of selectable options |
| `value` | `string[]` | `[]` | Currently selected values (controlled) |
| `onValueChange` | `(value: string[]) => void` | — | Called when selection changes |
| `placeholder` | `string` | `"Select options..."` | Shown when nothing is selected |
| `searchPlaceholder` | `string` | `"Search..."` | Placeholder inside search input |
| `emptyText` | `string` | `"No options found."` | Shown when search has no results |
| `disabled` | `boolean` | `false` | Disables the entire component |
| `maxCount` | `number` | — | Maximum number of selectable items |
| `clearable` | `boolean` | `true` | Show/hide the "Clear all" control |
| `className` | `string` | — | Extra classes for the trigger button |
| `contentClassName` | `string` | — | Extra classes for the dropdown panel |
| `aria-label` | `string` | — | Accessible label for screen readers |

---

## Keyboard Navigation

| Key | Action |
|---|---|
| `Space` / `Enter` | Open dropdown / select focused option |
| `↑` / `↓` | Navigate options |
| `Escape` | Close dropdown |
| `Backspace` | (in search) remove last selected item |
| `Tab` | Move focus out of dropdown |

---

## TypeScript Usage

All types are exported from the package:

```ts
import type { MultiSelectOption, MultiSelectProps } from "@parag.vora/react-multiselect-ui";

// Build options dynamically with full type safety
const buildOptions = (items: { id: string; name: string }[]): MultiSelectOption[] =>
  items.map((item) => ({ value: item.id, label: item.name }));
```

---

## Troubleshooting

### Component renders but has no styles

You're missing the `@source` directive in your CSS. Tailwind v4 tree-shakes classes by scanning your source files, but it won't scan `node_modules` by default.

```css
/* src/index.css */
@import "tailwindcss";
@source "../../node_modules/@parag.vora/react-multiselect-ui/dist";
```

The path `../../node_modules` assumes your CSS file is two directories deep (e.g. `src/`). Adjust based on your project structure:

```css
/* If CSS is at root level (e.g. styles/globals.css) */
@source "../node_modules/@parag.vora/react-multiselect-ui/dist";
```

---

### "Could not find module" error

Make sure all peer dependencies are installed:

```bash
npm install react@^19 react-dom@^19 tailwindcss@^4
```

---

### Dropdown doesn't appear / appears behind other elements

The dropdown is rendered in a Radix UI `Portal`, which mounts directly on `<body>`. If it's hidden behind a modal or sticky header, check your `z-index` stacking context. You can override the dropdown's z-index via `contentClassName`:

```tsx
<MultiSelect contentClassName="z-[9999]" ... />
```

---

### TypeScript error: "Module has no exported member"

Make sure you're importing from the correct path:

```ts
// ✅ Correct
import { MultiSelect } from "@parag.vora/react-multiselect-ui";
import type { MultiSelectOption } from "@parag.vora/react-multiselect-ui";

// ❌ Wrong — don't import from internal paths
import { MultiSelect } from "@parag.vora/react-multiselect-ui/dist/index.mjs";
```

---

### Using with Tailwind v3 (not supported)

This library targets **Tailwind v4 only**. If your project uses Tailwind v3, the `@source` directive won't work. You would need to add the `content` path to your `tailwind.config.js` instead:

```js
// tailwind.config.js (v3 only — not officially supported)
module.exports = {
  content: [
    "./src/**/*.{ts,tsx}",
    "./node_modules/@parag.vora/react-multiselect-ui/dist/**/*.{js,mjs}",
  ],
};
```

> Upgrading to Tailwind v4 is strongly recommended.

---

## Compatibility

| Tool | Version |
|---|---|
| React | 19+ |
| Tailwind CSS | v4+ |
| Node.js | 20+ |
| TypeScript | 5+ |
| Vite | 6+ / 7+ |
| Next.js | 15+ |

---

## License

MIT © [Parag Vora](https://github.com/pcvora23)