Multi Select

Form

Multi-value selector with tag-based display.

Preview

Select technologies...

Usage

example.jsx
import { MultiSelect } from "@/components/ui/multi-select";

export default function Example() {
  return <MultiSelect />;
}

Source Code

Copy this file into components/ui/multi-select.jsx in your project.

multi-select.jsx
"use client";

import { forwardRef, useState, useRef, useEffect } from "react";
import { cn } from "@/lib/utils";

const MultiSelect = forwardRef(
  ({ className, options = [], value = [], onValueChange, placeholder = "Select...", ...props }, ref) => {
    const [open, setOpen] = useState(false);
    const wrapperRef = useRef(null);

    useEffect(() => {
      const handleClick = (e) => {
        if (wrapperRef.current && !wrapperRef.current.contains(e.target)) setOpen(false);
      };
      document.addEventListener("mousedown", handleClick);
      return () => document.removeEventListener("mousedown", handleClick);
    }, []);

    const toggle = (val) => {
      const next = value.includes(val) ? value.filter((v) => v !== val) : [...value, val];
      onValueChange?.(next);
    };

    const remove = (val) => onValueChange?.(value.filter((v) => v !== val));

    const selectedLabels = options.filter((o) => value.includes(o.value));

    return (
      <div ref={wrapperRef} className={cn("relative", className)} {...props}>
        <div
          ref={ref}
          onClick={() => setOpen((o) => !o)}
          className="flex min-h-9 w-full flex-wrap items-center gap-1 rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm cursor-pointer"
        >
          {selectedLabels.length === 0 && (
            <span className="text-muted-foreground">{placeholder}</span>
          )}
          {selectedLabels.map((opt) => (
            <span key={opt.value} className="inline-flex items-center gap-1 rounded-md bg-secondary px-2 py-0.5 text-xs font-medium">
              {opt.label}
              <button type="button" onClick={(e) => { e.stopPropagation(); remove(opt.value); }} className="hover:text-destructive cursor-pointer">×</button>
            </span>
          ))}
        </div>
        {open && (
          <div className="absolute z-50 mt-1 max-h-60 w-full overflow-auto rounded-md border bg-popover p-1 shadow-md">
            {options.map((opt) => (
              <button
                key={opt.value}
                type="button"
                onClick={() => toggle(opt.value)}
                className={cn(
                  "flex w-full items-center gap-2 rounded-sm px-2 py-1.5 text-sm cursor-pointer hover:bg-accent",
                  value.includes(opt.value) && "bg-accent text-accent-foreground"
                )}
              >
                <span className={cn("flex h-4 w-4 items-center justify-center rounded border border-primary", value.includes(opt.value) && "bg-primary text-primary-foreground")}>
                  {value.includes(opt.value) && "✓"}
                </span>
                {opt.label}
              </button>
            ))}
          </div>
        )}
      </div>
    );
  }
);
MultiSelect.displayName = "MultiSelect";

export { MultiSelect };

Quick Install

Make sure you have the cn() utility set up. It requires clsx and tailwind-merge.

npm install clsx tailwind-merge