@synapxlab/multiselect

Sélection multiple à chips et recherche — le remplaçant de select2, sans jQuery.

  • ✓Chips et recherche en direct, « Tout sélectionner » limité aux lignes filtrées
  • ✓Le <select> natif reste la source de vérité : le formulaire se poste tel quel, sans JavaScript côté serveur
  • ✓Popover en position: fixed dans body — jamais coupé par un overflow, un tableau ou une modale
  • ✓Pilotage clavier complet et rôles ARIA (combobox, listbox, option)
  • ✓Thème sombre par custom properties CSS
TypeScript Vanilla ESM UMD 7 ko gz MIT
multiselect.ts
import { MultiSelect } from '@synapxlab/multiselect'; import '@synapxlab/multiselect/style'; // Habille chaque select[multiple] de la page MultiSelect.init('select[multiple]', { locale: 'fr', onChange(values) { console.log('sélection', values); }, });

Démo interactive

Le composant s'attache à un select existant : l'élément natif reste dans le DOM, masqué, et continue de porter la valeur. Chaque carte reproduit un usage courant.

1. Multiple, 15 options

Chips, recherche en direct et « Tout sélectionner » limité aux lignes filtrées.

2. Simple, valeur unique

Un select sans multiple reçoit le même habillage : pas de chips, fermeture au choix.

3. Présélection et maxItems

Deux valeurs choisies au départ, maxItems: 3, recherche désactivée.

4. Même habillage sans le composant

La classe sxms-select donne le même champ à un select natif, sans JavaScript.

5. Dans une boîte overflow: hidden

Le popover est rendu dans body en position: fixed : il n'est jamais coupé par la bordure pointillée.

6. API

Cible : le composant n° 1.

🎯 Pourquoi pas select2 ?

select2 traîne jQuery et un thème à reconstruire à la main pour chaque style. Ce composant reprend l'essentiel sans dépendance runtime.

Pas de jQuery

Vanilla TypeScript, ESM ou UMD, zéro dépendance à l'exécution.

Popover jamais coupé

Rendu dans document.body en position: fixed : un tableau scrollable, une modale avec transform ou un conteneur overflow: hidden ne le tronquent jamais.

Clavier et ARIA complets

combobox, listbox, option, aria-selected, aria-activedescendant.

Thème sombre

Custom properties CSS, sans reconstruire un thème jQuery UI.

✨ L'effet de sélection

La case à cocher d'une option est invisible tant qu'elle n'est pas choisie. Au clic, la ligne prend la couleur primaire en bordure, la case apparaît en fondu et le libellé glisse pour lui laisser la place — le tout animé en .3s. Inspiré du composant de formulaire de la plateforme inwink.

📦 Installation

npm install @synapxlab/multiselect

⚡ Démarrage rapide

import { MultiSelect } from '@synapxlab/multiselect';
import '@synapxlab/multiselect/style';

const ms = new MultiSelect(document.querySelector('select[name=attentes]'), {
  searchable: true,
  locale: 'fr',
  onChange(values) {
    console.log('sélection', values);
  },
});
<link rel="stylesheet" href="node_modules/@synapxlab/multiselect/dist/style.css">
<script src="node_modules/@synapxlab/multiselect/dist/multiselect.umd.cjs"></script>
<script>
  const { MultiSelect } = window.SynapxMultiSelect;
  MultiSelect.init('select[data-multiselect]');
</script>
<select name="pays" class="sxms-select">
  <option value="fr">France</option>
</select>

📖 Options

Le composant s'attache à un <select multiple> (chips, recherche, « Tout sélectionner ») ou à un <select> simple (valeur unique, fermeture au choix).

OptionTypeDéfautDescription
placeholderstringstrings.placeholderTexte affiché quand le champ est vide
searchablebooleantruefalse retire la barre de recherche
selectAllbooleantrueLigne « Tout sélectionner » (multiple seulement)
showSortbooleantrueBouton de tri à côté de la barre de recherche
sortSelectedFirstbooleanfalseÉtat initial de la bascule de tri
closeOnSelectbooleantrue en simple, false en multipleFerme le popover juste après un choix
maxItemsnumber—Limite de sélection (multiple seulement)
containerHTMLElementdocument.bodyParent du popover. Passer un <dialog> ouvert pour que le popover vive dans le top-layer
zIndexnumber1060z-index du popover
widthnumber | 'anchor'400Largeur du popover en px, ou 'anchor' pour reprendre la largeur du champ. Recalée dans le viewport
maxHeightnumber300Hauteur max de la liste d'options (px)
offsetnumber—Espace entre le champ et le popover (px)
viewportMarginnumber—Distance minimale aux bords du viewport (px)
placement'auto' | 'top' | 'bottom''auto'Côté préféré
mobileBreakpointnumber640Largeur en dessous de laquelle le popover devient une feuille plein écran
stringsPartial<MultiSelectStrings>défaut de la localeLibellés, voir le tableau ci-dessous
locale'fr' | 'en'langue du document si français, sinon anglaisChoisit le jeu de libellés intégré
renderOption(item, el) => void—Hook appelé sur chaque ligne d'option avant son insertion
onChange(values: string[], instance) => void—Sélection modifiée
onOpen / onClose(instance) => void—Popover ouvert / fermé

Libellés

Locales intégrées : en et fr. Chaque clé se surcharge par strings.

Cléenfr
placeholderClick to select an itemCliquez pour sélectionner un élément
searchSearch...Recherche...
selectAllSelect allTout sélectionner
noResultsNo resultsAucun résultat
removeRemoveSupprimer
sortSelectedFirstSelected firstSélectionnés en premier
closeCloseFermer
validateDoneValider
listLabelOptionsOptions

🔔 Événements & méthodes

Les événements personnalisés remontent sur le select natif, chacun avec detail: { values, instance }. Chaque changement déclenche aussi les événements natifs change et input sur le select : formulaires, bibliothèques de validation et frameworks réagissent comme avec le contrôle natif.

ÉvénementQuand
multiselect:openPopover ouvert
multiselect:closePopover fermé
multiselect:changeSélection modifiée
MéthodeDescription
MultiSelect.init(sélecteur, options)Crée une instance par <select> correspondant (idempotent), les renvoie
MultiSelect.get(select)Instance attachée à un select
getValue(): string[]Valeur courante
setValue(values: string[])Écrit le select natif, déclenche change
open() / close() / toggle() / isOpenPilotage du popover
refresh()Relit les éléments <option> depuis le DOM
setOptions(items)Réécrit les <option> depuis { value, label, disabled?, selected? }[]
disable() / enable()Désactive / réactive le champ
destroy()Retire tout élément et écouteur, rend le select natif visible

♿ Accessibilité et clavier

  • Champ : role="combobox", aria-expanded, aria-haspopup="listbox", aria-controls, aria-disabled. Le clic sur le <label for="..."> du select natif continue d'ouvrir le composant.
  • Liste : role="listbox", aria-multiselectable, lignes role="option" avec aria-selected et aria-disabled, aria-activedescendant suit la ligne active depuis le champ de recherche.
  • Clavier sur le champ : Entrée / Espace / Flèche bas ouvrent le popover, Retour arrière retire la dernière chip.
  • Clavier dans le popover : Flèche haut / Flèche bas déplacent, Entrée / Espace basculent, Échap ferme, Tab ferme et rend le focus, Origine / Fin sautent aux extrémités de la liste.
  • Le champ de recherche reçoit le focus à l'ouverture, sauf au tactile, où c'est la liste qui le reçoit pour ne pas ouvrir le clavier virtuel.
  • Le focus revient au champ à la fermeture. Un seul popover est ouvert à la fois.

🎨 Thème

Toutes les couleurs et mesures passent par des custom properties, préfixe sxms-. Aucun !important : surchargez les variables dans votre feuille de style. La palette sombre s'applique sous @media (prefers-color-scheme: dark), et sous [data-bs-theme="dark"] ou [data-theme="dark"] sur n'importe quel ancêtre.

VariableRôle
--sxms-bgFond du champ
--sxms-border / --sxms-border-hoverBordure au repos / au survol
--sxms-text / --sxms-mutedTexte / placeholder, chevron
--sxms-chip-bg / --sxms-chip-textChips du champ fermé
--sxms-primaryBordure et texte de la ligne cochée
--sxms-popup-bgFond du popover
--sxms-item-bg / --sxms-item-hover / --sxms-item-hover-borderLigne d'option, au repos et au survol
--sxms-shadowOmbre du popover
--sxms-focusAnneau de focus clavier
Select natif, même habillage : pour un <select> qui n'a pas besoin du composant, la classe sxms-select lui donne le même champ, le même chevron et les mêmes tokens, sans JavaScript.

Nécessite ES2020, le sélecteur CSS :has() et la propriété CSS translate : Chrome/Edge 105+, Safari 15.4+, Firefox 121+.