Nous developpons votre site vitrine SPA À PARTIR DE 300€. Oui, c'est dingue. Web SPA À PARTIR DE 300€ — Oui, c'est dingue. Parlons-en →

Modales et notifications sans Bootstrap ni Tailwind, en CSS pur

Modales et notifications sans Bootstrap ni Tailwind, en CSS pur

Afficher une boîte de dialogue et une notification fait partie des choses qu'on règle le plus vite en important un framework entier, et qu'on paie le plus cher ensuite. Vous verrez ici ce qu'il faut vraiment pour qu'une modale fonctionne bien, où se situe le travail réel, qui n'est pas l'apparence, et comment tout se résout avec des variables CSS et une poignée de lignes de JavaScript, sans vous lier à un système de design. Le CSS et le JavaScript sont standards et fonctionnent partout ; seule l'intégration optionnelle de la fin est en Laravel, l'un de nos terrains chez AndorraDev.

Une modale, c'est trois choses et aucune n'a besoin d'un framework

Quand quelqu'un a besoin d'une boîte de dialogue et d'une notification, le réflexe habituel est d'importer une librairie qui traîne en plus son propre système de design. Or ce qu'il faut vraiment est bien moins que ça.

Une modale, c'est une couche qui masque, un panneau qui apparaît et un focus qui reste à l'intérieur. Une notification, c'est un élément qui entre, attend et repart. Aucune de ces quatre choses n'exige un framework CSS : elles exigent quelques règles et un peu de discipline.

Le problème, si on le résout avec ce qu'on a déjà sous la main, c'est que ça vous lie au framework. Avec Bootstrap, sa modale arrive gratuitement et son esthétique aussi, et l'en sortir revient à la réécrire. Avec Tailwind, charger Bootstrap juste pour la boîte de dialogue, ce sont deux systèmes de design qui cohabitent sur la même page, avec leurs deux resets qui se disputent.

Le JavaScript d'une modale tient en deux cents lignes

Ça surprend, jusqu'à ce qu'on le raconte. Le système complet de modales et de notifications que nous maintenons, ce sont 363 lignes de JavaScript, 157 pour les notifications et 206 pour les boîtes de dialogue, sans une seule dépendance npm. Les deux fichiers sont des fonctions auto-exécutées, sans imports, sans build obligatoire.

Ça tient parce que l'essentiel du travail n'est pas du JavaScript. C'est du CSS.

Tout l'état vit dans un attribut, et le CSS fait le reste

La plupart des implémentations maison finissent avec du JavaScript qui écrit directement des styles, et c'est là qu'elles deviennent impossibles à thématiser. Il existe une alternative qui coûte autant à écrire : que le JavaScript se contente de déclarer l'état de la boîte de dialogue et laisse toute l'apparence au CSS.

modal.setAttribute('data-wm-state', state);
modal.setAttribute('aria-hidden', state === 'closed' ? 'true' : 'false');

L'opacité, la visibilité, la transformation d'entrée, le défilement interne et le comportement sur mobile sont résolus par des sélecteurs du type [data-wm-state="open"]. Cela a trois conséquences pratiques.

Vous pouvez changer l'animation sans toucher au JavaScript. C'est une règle CSS de plus.

L'état est inspectable. Vous ouvrez les outils du navigateur et vous voyez si la boîte de dialogue est ouverte sans poser un point d'arrêt.

Et sur mobile, aucune logique n'est nécessaire. En dessous de 640px le panneau passe en plein écran avec une media query, pas avec une vérification de largeur en JavaScript.

Andie recommande

L'enveloppe de la modale porte déjà wire:ignore.self, ce qui empêche un rendu Livewire d'écraser l'attribut d'état pendant que l'utilisateur saisit dans un champ. C'est un petit détail et c'est la différence entre un dialogue qui tient dans un formulaire réactif et un qui ne tient pas. Si un jour vous construisez le vôtre à la main, c'est la ligne à ne pas oublier.

Le focus est la partie que presque personne n'implémente

Une boîte de dialogue accessible doit faire trois choses au clavier, et la plupart des implémentations maison n'en font aucune.

Piéger la tabulation à l'intérieur du panneau, pour ne pas partir naviguer dans la page qui se trouve derrière. Rendre le focus à l'élément qui l'a ouverte à la fermeture. Et respecter un autofocus explicite s'il y en a un, au lieu de sauter systématiquement au premier élément focusable.

Les trois sont réglées : le piège de focus intercepte Tab et Shift+Tab sur la modale la plus haute de la pile, et l'élément d'origine est conservé dans un WeakMap pour le restaurer à la fermeture. C'est le genre de détail qui ne se voit pas dans une démo et qui se remarque quand quelqu'un navigue sans souris.

Les messages s'injectent avec textContent, pas avec innerHTML

Une notification affiche du texte qui vient souvent de l'extérieur : un nom de fichier, un message d'erreur, quelque chose qu'un utilisateur a écrit. Une seule ligne décide si cela s'affiche comme du texte ou s'exécute comme du HTML :

if (title) toast.querySelector('.wt-title').textContent = title;
toast.querySelector('.wt-body').textContent = message;

Cela veut dire que vous ne pouvez pas mettre de gras dans une notification, et c'est intentionnel. En échange, un message provenant de données de l'utilisateur ne peut rien exécuter. Dans un système où les notifications affichent souvent des messages de validation construits à partir de ce que l'utilisateur a saisi, cette décision évite tout un problème.

Les icônes sont également embarquées, sous forme de constantes : pas de dépendance à une librairie d'icônes ni de requête supplémentaire pour afficher une notification.

Un seul attribut aligne toute l'interface

La partie la plus rentable au quotidien, c'est le système de thèmes en deux couches.

Il y a des méta-variables partagées par toute la famille de composants, du type --wire-bg, --wire-text, --wire-accent, --wire-radius, --wire-font. Et ensuite chaque composant définit les siennes en les dérivant :

:root {
    --wire-accent: #2563eb;
    --wire-radius: 10px;
}

.wt-container {
    --wt-radius: var(--wire-radius);
    --wt-bg: var(--wire-bg-elevated);
}

Le résultat pratique, c'est que vous écrivez data-wire-theme="studio" sur la balise <html> et les boîtes de dialogue, les notifications, la bannière de cookies et le widget de tickets s'alignent tout seuls. Et pour y mettre votre marque, pas besoin d'écraser des sélecteurs en se battant avec la spécificité : vous redéfinissez une demi-douzaine de variables.

Il y a onze thèmes inclus, mais l'important n'est pas le catalogue. C'est que la surface de personnalisation soit constituée de variables CSS et non de classes, parce que c'est ce qui permet de changer l'apparence sans toucher au paquet ni bloquer la mise à jour.

Livewire optionnel veut dire vérifier la classe avant d'enregistrer quoi que ce soit

Déclarer une dépendance comme optionnelle dans le composer.json n'est que la moitié du travail. L'autre moitié consiste à ce que le code se comporte vraiment comme si elle pouvait être absente, et c'est là que trébuchent pas mal de paquets qui se disent agnostiques.

Livewire n'est pas dans les dépendances obligatoires, il est en suggestion. Et le fournisseur de services vérifie que la classe existe avant d'enregistrer le raccourci :

if (! class_exists(\Livewire\Component::class)) {
    return;
}

\Livewire\Component::macro('notify', function (...) { ... });

Avec Livewire, vous écrivez $this->notify('Guardado', 'success') depuis le composant PHP. Sans Livewire, vous appelez window.notify(...) ou vous déclenchez le même événement depuis Alpine, et le paquet fonctionne pareil. La macro ne fait pas de magie : elle traduit en un envoi d'événement que le JavaScript écoute sur window. C'est tout le couplage.

Ce qui existe sur le marché et en quoi c'est différent

Il vaut mieux être précis sur les compromis de chaque option, parce que toutes sont raisonnables dans leur contexte.

La modale de Bootstrap est solide et éprouvée sur des millions de sites. Si vous chargez déjà Bootstrap en entier, aucun intérêt à ajouter quoi que ce soit. Le compromis apparaît quand vous ne le chargez pas : l'importer pour la boîte de dialogue, ce sont son JavaScript et son reset de styles complets, et son apparence est reconnaissable, donc la personnaliser en profondeur demande du travail.

SweetAlert2 fait bien plus qu'une boîte de dialogue, et le fait bien. En échange, il impose son propre langage visuel et sa personnalisation passe par une lutte avec ses classes et ses options. C'est le choix naturel si vous voulez des boîtes de dialogue très fournies en fonctionnalités d'origine et que ça ne vous dérange pas qu'on voie d'où elles viennent.

Toastr couvre les notifications et fonctionne depuis des années, mais dépend de jQuery, qui est aujourd'hui une dépendance que beaucoup de projets n'ont plus. Notyf ne dépend de rien et est minuscule, et sa surface de personnalisation se limite à une poignée d'options.

La différence d'approche qui nous convient est ailleurs : les notifications et les boîtes de dialogue partagent le même système de variables, elles se stylent donc une seule fois et restent cohérentes, et le même interrupteur de thème entraîne aussi la bannière de cookies et le widget de tickets. Quand le produit a sept gabarits différents, c'est la différence entre une modification et sept.

À quoi ça ressemble en production

Sur Crowd Legal, il y a 267 appels aux notifications répartis dans 58 fichiers et 24 boîtes de dialogue dans 24 vues, toutes sous le même thème déclaré une seule fois sur la balise <html>. Sur Abodara, ce sont 636 appels dans 173 fichiers.

Et une donnée qui répond à la question évidente : sur Crowd Legal, le CSS de ces composants est importé avant les directives de Tailwind, dans le même fichier. Ils cohabitent sans collision parce que chacun utilise son préfixe de classe. Qu'ils soient indépendants du framework ne veut pas dire que vous ne pouvez pas les utiliser à côté d'un framework.

Et si vous ne travaillez pas avec Laravel

Les critères se transposent, et ils servent à juger n'importe quelle librairie de boîtes de dialogue que vous envisagez :

  • Que l'état vive dans un attribut et l'apparence dans le CSS. Si la librairie écrit des styles en ligne depuis JavaScript, la personnaliser sera un combat.
  • Que la personnalisation passe par des variables, pas par des classes. Écraser des sélecteurs qui ne sont pas les vôtres casse à chaque mise à jour.
  • Qu'elle piège le focus et le rende. Sans cela, la boîte de dialogue n'est pas accessible, aussi jolie soit-elle.
  • Et que le contenu soit injecté comme du texte. Si votre librairie de notifications accepte du HTML par défaut, vous avez déjà un vecteur ouvert dès qu'un message inclut des données de l'utilisateur.

Où se trouve le code

Les deux paquets sont sur Packagist (wiretoast, wiremodal), le code sur GitHub (wiretoast, wiremodal) et la documentation des paquets sur wiretoast et wiremodal. Il y a une démo interactive pour voir les thèmes sans rien installer. Ils sont sous licence MIT.

Si vous montez une application et que vous ne voulez pas que l'interface vous lie à un framework de styles, c'est l'une des décisions les moins chères à bien prendre dès le départ. Nous la travaillons en conception web et en applications web, et si vous nous parlez du projet, nous vous dirons ce qui convient.

Écrit par
Edu Lazaro
Edu Lazaro
Founder & Lead Developer en AndorraDev

Développeur full-stack avec plus de 15 ans d'expérience en Laravel, React, Node.js et architectures cloud. J'aide les entreprises en Andorre à construire leur présence digitale.

Partner de diseño · ionospace.
Besoin d'aide? ×
Andie by AndorraDev
Assistant IA + équipe humaine
Assistant IA d'AndorraDev
Andie
Bonjour! Je suis Andie, l'assistant IA d'AndorraDev. Comment puis-je vous aider? Si vous souhaitez parler avec Edu, il suffit de le demander.
17:27