React + MotionFilters
Multi-select dropdown
A dropdown with a search box at the top where you tick several options and see how many are picked.
Settings
6
Code
import { useState } from "react";
import { AnimatePresence, motion } from "framer-motion";
import { Check, ChevronDown, Search } from "lucide-react";
const sizeMap = {
sm: { pad: "px-3 py-1.5", text: "text-xs" },
md: { pad: "px-4 py-2", text: "text-sm" },
lg: { pad: "px-5 py-2.5", text: "text-base" },
} as const;
interface MultiSelectProps {
accent?: string;
size?: keyof typeof sizeMap;
options?: string[];
value?: string[];
onChange?: (value: string[]) => void;
}
export default function MultiSelect({
accent = "#0071e3",
size = "md",
options = ["Design", "Code", "Audio", "Video", "3D", "Data"],
value,
onChange,
}: MultiSelectProps) {
const [open, setOpen] = useState(false);
const [query, setQuery] = useState("");
const [internal, setInternal] = useState<string[]>([]);
const selected = value ?? internal;
const s = sizeMap[size];
const filtered = options.filter((o) => o.toLowerCase().includes(query.toLowerCase()));
const toggle = (o: string) => {
const next = selected.includes(o) ? selected.filter((x) => x !== o) : [...selected, o];
if (value === undefined) setInternal(next);
onChange?.(next);
};
return (
<div className="relative w-64">
<button
type="button"
aria-expanded={open}
onClick={() => setOpen((o) => !o)}
className={`flex w-full items-center justify-between rounded-full border border-black/10 bg-white font-medium dark:border-white/10 dark:bg-[#1c1c1e] ${s.pad} ${s.text}`}
>
<span className={selected.length ? "" : "text-[#86868b]"}>
{selected.length ? `${selected.length} seleccionados` : "Select..."}
</span>
<ChevronDown className={`h-4 w-4 transition-transform ${open ? "rotate-180" : ""}`} />
</button>
<AnimatePresence>
{open && (
<motion.div
initial={{ opacity: 0, y: -6 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -6 }}
className="absolute z-20 mt-2 w-full overflow-hidden rounded-2xl border border-black/10 bg-white shadow-[0_1px_2px_rgba(0,0,0,0.03),0_8px_28px_rgba(0,0,0,0.05)] dark:border-white/10 dark:bg-[#1d1d1f]"
>
<div className="flex items-center gap-2 border-b border-black/10 px-3 py-2 dark:border-white/10">
<Search className="h-3.5 w-3.5 text-[#86868b]" />
<input
autoFocus
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
className="w-full bg-transparent text-sm outline-none placeholder:text-[#86868b]"
/>
</div>
<div className="max-h-44 overflow-y-auto p-1 [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-black/20 dark:[&::-webkit-scrollbar-thumb]:bg-white/20">
{filtered.map((o) => {
const on = selected.includes(o);
return (
<button
key={o}
type="button"
aria-pressed={on}
onClick={() => toggle(o)}
className="flex w-full items-center justify-between rounded-md px-2.5 py-1.5 text-sm hover:bg-black/5 dark:hover:bg-white/10"
>
{o}
{on && <Check className="h-4 w-4" style={{ color: accent }} />}
</button>
);
})}
{!filtered.length && (
<p className="px-2.5 py-2 text-sm text-[#86868b]">No results</p>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Prompt for AI
Paste it into ChatGPT, Claude or Cursor and it will adapt the block to your project with these settings, even if you don’t use React.
More in Filters
Where to use it
When there are too many options for chips, a dropdown keeps the page tidy. Think of choosing languages in a translator’s profile, tags for a blog post or teammates to assign a task to. The search box filters as you type, so this multi-select dropdown still feels quick with a long list.
The button shows a count rather than every name, which keeps it compact. If people need to see their choices at a glance, show them as chips under the field. And swap the placeholder for something specific, like “Pick languages”.