npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

react-multiselect-ui

v2.1.0

Published

ShadCN-style accessible Multi Select for React 17/18/19 with Tailwind CSS v3/v4

Readme

react-multiselect-ui

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

npm version npm downloads license TypeScript


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)
  • ✅ Clear all / remove individual selections
  • ✅ TypeScript — fully typed props and exports
  • ✅ ShadCN-compatible Tailwind styling
  • ✅ No lucide-react dependency — uses inline SVGs
  • ✅ Supports React 17, 18, and 19
  • ✅ Supports Tailwind CSS v3 and v4

Compatibility

| Tool | Supported Versions | |---|---| | React | 17, 18, 19 | | Tailwind CSS | v3, v4 | | TypeScript | 4.x, 5.x | | Node.js | 18+ |


Installation

# npm
npm install react-multiselect-ui

# yarn
yarn add react-multiselect-ui

# pnpm
pnpm add react-multiselect-ui

Peer Dependencies

Make sure these are already in your project:

npm install react react-dom

Tailwind Setup

This step is required for the component to be styled correctly. Skip it and the component will render unstyled.

Tailwind v4

Add the @source directive so Tailwind scans the component's class names:

/* src/index.css or src/globals.css */
@import "tailwindcss";

/* Required — tells Tailwind v4 to scan the package */
@source "../../node_modules/react-multiselect-ui/dist";

Adjust the relative path based on where your CSS file lives:

  • CSS at src/index.css → use ../../node_modules/...
  • CSS at root styles/globals.css → use ../node_modules/...
  • CSS at root index.css → use ./node_modules/...

Tailwind v3

Add the dist path to the content array in tailwind.config.js:

// tailwind.config.js
module.exports = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx}",
    // Required — tells Tailwind v3 to scan the package
    "./node_modules/react-multiselect-ui/dist/**/*.{js,mjs}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};

Quick Start

import { useState } from "react";
import { MultiSelect } from "react-multiselect-ui";

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

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

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

Examples

1. Basic Usage

import { useState } from "react";
import { MultiSelect } from "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. Pre-selected Values

Initialize useState with values to show defaults on first render:

export function PreselectedExample() {
  const [languages, setLanguages] = useState<string[]>(["en", "es"]);

  return (
    <MultiSelect
      options={[
        { value: "en", label: "English" },
        { value: "es", label: "Spanish" },
        { value: "fr", label: "French" },
        { value: "de", label: "German" },
        { value: "zh", label: "Chinese" },
      ]}
      value={languages}
      onValueChange={setLanguages}
      placeholder="Select languages..."
    />
  );
}

3. Max Selection Limit

Use maxCount to cap how many items can be selected. Options automatically disable once the limit is reached:

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

  return (
    <div className="w-80">
      <MultiSelect
        options={[
          { value: "cheese",    label: "Extra Cheese" },
          { value: "mushrooms", label: "Mushrooms" },
          { value: "peppers",   label: "Bell Peppers" },
          { value: "onions",    label: "Onions" },
          { value: "olives",    label: "Olives" },
        ]}
        value={toppings}
        onValueChange={setToppings}
        placeholder="Select toppings..."
        maxCount={3}
      />
      {toppings.length === 3 && (
        <p className="mt-1 text-xs text-amber-600">
          Maximum 3 toppings reached!
        </p>
      )}
    </div>
  );
}

4. Disabled Component

Disable the entire component conditionally:

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

  return (
    <div className="space-y-2 w-80">
      <MultiSelect
        options={[
          { value: "admin",  label: "Admin" },
          { value: "editor", label: "Editor" },
          { value: "viewer", label: "Viewer" },
          { value: "guest",  label: "Guest" },
        ]}
        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>
  );
}

5. Disabled Individual Options

Mark specific options as unselectable while keeping others available:

const PLANS = [
  { value: "free",       label: "Free Tier" },
  { value: "pro",        label: "Pro" },
  { value: "team",       label: "Team" },
  // This option cannot be selected
  { value: "enterprise", label: "Enterprise — contact sales", disabled: true },
];

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

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

6. Inside a Form with Reset

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">
      <MultiSelect
        options={[
          { 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" },
        ]}
        value={skills}
        onValueChange={setSkills}
        placeholder="Select your skills..."
      />
      <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>
  );
}

7. Without Clear Button

Hide the "Clear all" control so users can only deselect via individual badge X buttons:

<MultiSelect
  options={options}
  value={selected}
  onValueChange={setSelected}
  placeholder="Add labels..."
  clearable={false}
/>

8. Custom Search and Empty Text

<MultiSelect
  options={options}
  value={selected}
  onValueChange={setSelected}
  placeholder="Choose team members..."
  searchPlaceholder="Type a name to search..."
  emptyText="No team members match your search."
/>

9. Custom Styling

Use className to style the trigger button and contentClassName for the dropdown panel:

<MultiSelect
  options={options}
  value={selected}
  onValueChange={setSelected}
  placeholder="Pick colors..."
  className="border-violet-300 focus-visible:ring-violet-500"
  contentClassName="border-violet-200"
/>

10. Async / API-loaded Options

import { useState, useEffect } from "react";
import { MultiSelect } from "react-multiselect-ui";
import type { MultiSelectOption } from "react-multiselect-ui";

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

  useEffect(() => {
    fetch("/api/users")
      .then((res) => res.json())
      .then((data) => {
        setOptions(
          data.map((u: { id: string; name: string }) => ({
            value: u.id,
            label: u.name,
          }))
        );
        setLoading(false);
      });
  }, []);

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

11. Accessible with ARIA Label

<MultiSelect
  options={options}
  value={selected}
  onValueChange={setSelected}
  placeholder="Filter by department..."
  aria-label="Filter results by department"
/>

12. Controlled Reset from Parent

export function ParentControlExample() {
  const [selected, setSelected] = useState<string[]>(["react", "typescript"]);

  return (
    <div className="space-y-3 w-80">
      <MultiSelect
        options={options}
        value={selected}
        onValueChange={setSelected}
        placeholder="Select technologies..."
      />
      <div className="flex gap-2">
        <button onClick={() => setSelected([])}>
          Clear All
        </button>
        <button onClick={() => setSelected(["react", "typescript", "node"])}>
          Reset to Defaults
        </button>
      </div>
    </div>
  );
}

API Reference

MultiSelectOption

interface MultiSelectOption {
  value: string;       // Unique identifier stored in the value array
  label: string;       // Display text shown in the dropdown and badges
  disabled?: boolean;  // Prevents this option from being selected
}

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 the 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 through options | | Escape | Close the dropdown | | Tab | Move focus out of the dropdown |


TypeScript

All types are exported from the package root:

import { MultiSelect } from "react-multiselect-ui";
import type { MultiSelectProps, MultiSelectOption } from "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 }));

Framework Guides

Next.js App Router

Add "use client" at the top of any file that uses the component since it uses React state:

"use client";

import { useState } from "react";
import { MultiSelect } from "react-multiselect-ui";

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

  return (
    <MultiSelect
      options={options}
      value={selected}
      onValueChange={setSelected}
    />
  );
}

Next.js Pages Router

Works without any extra configuration:

import { useState } from "react";
import { MultiSelect } from "react-multiselect-ui";

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

  return (
    <MultiSelect
      options={options}
      value={selected}
      onValueChange={setSelected}
    />
  );
}

Vite + React

Works out of the box. Just follow the Tailwind setup above.

Remix

// app/routes/_index.tsx
import { useState } from "react";
import { MultiSelect } from "react-multiselect-ui";

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

  return (
    <MultiSelect
      options={options}
      value={selected}
      onValueChange={setSelected}
    />
  );
}

Troubleshooting

Component renders with no styles

You are missing the Tailwind scan configuration. See the Tailwind Setup section above for your version (v3 or v4).

Dropdown appears behind modals or sticky headers

The dropdown uses a Radix UI Portal (mounts directly on <body>). Override the z-index via contentClassName:

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

TypeScript error: Cannot find module

Make sure you import from the package root:

// ✅ Correct
import { MultiSelect } from "react-multiselect-ui";
import type { MultiSelectOption } from "react-multiselect-ui";

// ❌ Wrong — never import from internal paths
import { MultiSelect } from "react-multiselect-ui/dist/index.mjs";

React 17 — JSX transform error

Make sure your tsconfig has the new JSX transform:

{
  "compilerOptions": {
    "jsx": "react-jsx"
  }
}

Migrating from @parag.vora/react-multiselect-ui

# Remove old package
npm uninstall @parag.vora/react-multiselect-ui

# Install new package
npm install react-multiselect-ui

Update your imports:

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

// After
import { MultiSelect } from "react-multiselect-ui";

The API is identical — no other changes needed.


Contributing

Contributions, issues, and feature requests are welcome!

  1. Fork the repository
  2. Create your branch: git checkout -b feat/my-feature
  3. Commit: git commit -m "feat: add my feature"
  4. Push: git push origin feat/my-feature
  5. Open a Pull Request on GitHub

License

MIT © Parag Vora


Links