Mehrfachauswahl-Dropdown
Ein Dropdown mit Suchfeld oben, in dem du mehrere Optionen abhakst und siehst, wie viele gewählt sind.
Einstellungen
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", "Daten"],
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` : "Auswählen..."}
</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="Suchen ..."
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]">Keine Ergebnisse</p>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Prompt für KI
Füg ihn in ChatGPT, Claude oder Cursor ein und der Baustein wird mit diesen Einstellungen an dein Projekt angepasst, auch ohne React.
Mehr aus Filter
Wofür du ihn nutzen kannst
Wenn es für Chips zu viele Optionen gibt, hält ein Dropdown die Seite aufgeräumt. Etwa bei Sprachen im Profil eines Übersetzers, Tags für einen Blogartikel oder Leuten, denen man eine Aufgabe zuweist. Das Suchfeld filtert beim Tippen, deshalb bleibt dieses Multi-Select-Dropdown auch bei langen Listen flott.
Der Button zeigt eine Zahl statt aller Namen und bleibt so kompakt. Müssen Leute ihre Auswahl auf einen Blick sehen, zeig sie als Chips darunter. Und ersetz den Platzhalter durch etwas Konkretes wie „Sprachen wählen“.