React + MotionFiltres

Liste déroulante multiple

Une liste déroulante avec recherche, où l’on coche plusieurs options et voit combien sont choisies.

Réglages

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", "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 ».