Liste déroulante multiple
Une liste déroulante avec recherche, où l’on coche plusieurs options et voit combien sont choisies.
Réglages
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", "Vidéo", "3D", "Données"],
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` : "Sélectionner..."}
</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="Rechercher..."
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]">Aucun résultat</p>
)}
</div>
</motion.div>
)}
</AnimatePresence>
</div>
);
}
Prompt pour l’IA
Collez-le dans ChatGPT, Claude ou Cursor : le bloc sera adapté à votre projet avec ces réglages, même sans React.
Plus dans Filtres
Où l’utiliser
Quand il y a trop d’options pour des chips, une liste déroulante garde la page ordonnée. Pensez au choix des langues dans le profil d’un traducteur, aux étiquettes d’un article de blog ou aux personnes à qui confier une tâche. La recherche filtre pendant la saisie, et ce select multiple reste rapide même avec une longue liste.
Le bouton affiche un nombre plutôt que tous les noms, ce qui le garde compact. Si l’on doit voir ses choix d’un coup d’œil, affichez-les en chips juste en dessous. Remplacez aussi le texte indicatif par quelque chose de précis, comme « Choisir des langues ».