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).
| Option | Type | Défaut | Description |
|---|---|---|---|
placeholder | string | strings.placeholder | Texte affiché quand le champ est vide |
searchable | boolean | true | false retire la barre de recherche |
selectAll | boolean | true | Ligne « Tout sélectionner » (multiple seulement) |
showSort | boolean | true | Bouton de tri à côté de la barre de recherche |
sortSelectedFirst | boolean | false | État initial de la bascule de tri |
closeOnSelect | boolean | true en simple, false en multiple | Ferme le popover juste après un choix |
maxItems | number | — | Limite de sélection (multiple seulement) |
container | HTMLElement | document.body | Parent du popover. Passer un <dialog> ouvert pour que le popover vive dans le top-layer |
zIndex | number | 1060 | z-index du popover |
width | number | 'anchor' | 400 | Largeur du popover en px, ou 'anchor' pour reprendre la largeur du champ. Recalée dans le viewport |
maxHeight | number | 300 | Hauteur max de la liste d'options (px) |
offset | number | — | Espace entre le champ et le popover (px) |
viewportMargin | number | — | Distance minimale aux bords du viewport (px) |
placement | 'auto' | 'top' | 'bottom' | 'auto' | Côté préféré |
mobileBreakpoint | number | 640 | Largeur en dessous de laquelle le popover devient une feuille plein écran |
strings | Partial<MultiSelectStrings> | défaut de la locale | Libellés, voir le tableau ci-dessous |
locale | 'fr' | 'en' | langue du document si français, sinon anglais | Choisit 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é | en | fr |
|---|---|---|
placeholder | Click to select an item | Cliquez pour sélectionner un élément |
search | Search... | Recherche... |
selectAll | Select all | Tout sélectionner |
noResults | No results | Aucun résultat |
remove | Remove | Supprimer |
sortSelectedFirst | Selected first | Sélectionnés en premier |
close | Close | Fermer |
validate | Done | Valider |
listLabel | Options | Options |
🔔 É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énement | Quand |
|---|---|
multiselect:open | Popover ouvert |
multiselect:close | Popover fermé |
multiselect:change | Sélection modifiée |
| Méthode | Description |
|---|---|
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() / isOpen | Pilotage 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, lignesrole="option"avecaria-selectedetaria-disabled,aria-activedescendantsuit la ligne active depuis le champ de recherche. - Clavier sur le champ :
Entrée/Espace/Flèche basouvrent le popover,Retour arrièreretire la dernière chip. - Clavier dans le popover :
Flèche haut/Flèche basdéplacent,Entrée/Espacebasculent,Échapferme,Tabferme et rend le focus,Origine/Finsautent 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.
| Variable | Rôle |
|---|---|
--sxms-bg | Fond du champ |
--sxms-border / --sxms-border-hover | Bordure au repos / au survol |
--sxms-text / --sxms-muted | Texte / placeholder, chevron |
--sxms-chip-bg / --sxms-chip-text | Chips du champ fermé |
--sxms-primary | Bordure et texte de la ligne cochée |
--sxms-popup-bg | Fond du popover |
--sxms-item-bg / --sxms-item-hover / --sxms-item-hover-border | Ligne d'option, au repos et au survol |
--sxms-shadow | Ombre du popover |
--sxms-focus | Anneau de focus clavier |
<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+.