Remplacer les bibliothèques de tooltips par l’API Popover et le positionnement des ancrages en CSS
Pourquoi Popper et l’interface flottante sont nécessaires aux aides contextuelles, fonctionnalités natives qui résolvent désormais les problèmes de superposition, de positionnement et de fermeture, ainsi que dans quels cas une bibliothèque JavaScript reste utile.
Afficher une ligne de texte à côté d’un bouton semble trivial, pourtant des outils comme Popper.js, Floating UI ainsi que de nombreux packages d’encapsulation existent parce que cela n’a jamais été possible auparavant. Une boîte à outils est en réalité l’assemblage de trois problèmes distincts superposés les uns aux autres, et jusqu’à récemment la plateforme ne proposait aucune solution déclarative pour aucun d’eux. Ce guide analyse ces problèmes, montre quel est le coût en JavaScript exécuté pour chacun d’eux, et les relie aux deux fonctionnalités des navigateurs qui couvrent désormais les cas courants : l’API Popover et le positionnement par ancre CSS. À la fin, vous saurez quels éléments d’une bibliothèque de boîtes à outils vous pouvez supprimer et quels éléments vous avez encore besoin.
Trois problèmes cachés au sein d’une seule boîte à outils
Chacun de ces problèmes représente une limitation ancienne et bien connue liée au comportement des navigateurs avant l’arrivée des nouvelles spécifications :
- Superposition. Le tooltip s’affichera-t-il au-dessus de tout le reste, ou sera-t-il coupé par un
overflow: hiddend’un ancêtre ? - Positionnement. Le tooltip sait-il où se trouve son déclencheur à l’écran, et suit-il cette position lors du scroll ou du redimensionnement ?
- Fermeture. Se ferme-t-il automatiquement lorsque l’utilisateur clique ailleurs ou appuie sur Escape ?
Pendant la majeure partie de l’histoire du web, tout projet ayant besoin d’un tooltip, d’une liste déroulante ou d’une liste de complétion résolvait ces trois problèmes en JavaScript. Les solutions varient selon chaque cas, et traiter ces éléments comme un seul problème explique précisément comment un petit détail d’interface s’est transformé en dépendance. Il est donc utile de les examiner un par un.
Superposition : pourquoi z-index ne peut pas sortir de son contexte
Chaque élément appartient à un contexte de superposition, qui détermine ce qui est dessiné par-dessus quoi. z-index ordonne les éléments au sein d’un même contexte de superposition ; il ne peut pas extraire un élément du contexte auquel il appartient.
Mettez la boîte à outils à l’intérieur d’un conteneur avec overflow: hidden, ou à l’intérieur d’une fenêtre modale qui crée son propre contexte de superposition, et aucune valeur, même pas 999999, ne la fera apparaître au-dessus de ces limites. La boîte à outils est soumise aux règles de rendu de ses ancêtres, et z-index n’a aucun effet en dehors d’elles.
L’issue de secours traditionnelle était un portal : il fallait placer le markup de l’info-bulle en un autre endroit du document, généralement à la fin de <body>, afin qu’elle n’hérite plus du cadrage et de l’empilement de son élément parent. C’est principalement pour cette raison que React dispose de la fonction createPortal. Il s’agit moins d’une fonctionnalité propre à React que d’une solution de contournement pour ce que le CSS ne pouvait pas exprimer.
Positionnement : le positionnement absolu ne prend en compte que les ancêtres
position: absolute place un élément par rapport à son ancêtre positionné le plus proche, c’est-à-dire l’élément le plus élevé dans la hiérarchie dont la propriété position est relative, absolute, fixed ou sticky. Le mot clé est ancêtre : la référence doit se trouver dans la même branche du DOM, quelque part au-dessus de l’info-bulle.
Dès que la boîte à outils et son déclencheur deviennent frères, ou que la boîte à outils a été déplacée vers <body> pour résoudre le problème de superposition, le déclencheur n’est plus un ancêtre. CSS ne disposait pas de moyen pour indiquer « positionnez ceci par rapport à cet élément non apparenté là-bas ». La faiblesse ne résidait pas dans la mise en position CSS en général, mais plutôt dans l’absence de toute relation de mise en position qui ignorerait la structure du document.
Les bibliothèques ont comblé cette lacune en effectuant des mesures. Elles appellent la fonction getBoundingClientRect() afin d’obtenir le cadre du déclencheur par rapport à la zone de visualisation, déduisent les coordonnées de la boîte à outils, et réexécutent ce calcul à chaque défilement ou redimensionnement, car les valeurs changent constamment. Ce cycle continu de mesure et de placement constitue l’essentiel du fonctionnement d’une bibliothèque de mise en position en temps réel.
Fermeture : comportement que le marquage ne pouvait pas décrire
Au préalable à l’API Popover, HTML et CSS ne disposaient d’aucun mécanisme pour « fermer l’élément lorsqu’utilisateur clique à l’extérieur ou appuie sur Escape ». Tout était géré par du code : un écouteur de clic sur document qui vérifiait si la cible de l’événement se trouvait en dehors du tooltip, un écouteur keydown qui attendait la touche Escape, ainsi que des opérations de nettoyage pour les deux cas lorsque le composant était désinstallé afin d’éviter tout fuite mémoire. Contrairement aux deux premiers problèmes, celui-ci ne concerne pas la géométrie ; il s’agit purement d’un comportement, mais cela représente néanmoins un troisième ensemble de code en temps de exécution que le navigateur ne fournissait pas.
Popovers versus modals
Il est utile de définir précisément la terminologie avant d’examiner la syntaxe. Un popover est tout élément affiché au-dessus du reste de la page, positionné par rapport à un déclencheur, et qui se ferme automatiquement lors d’un clic en dehors ou en appuyant sur la touche Escape. Les aides contextuelles, les menus déroulants, les listes de complétion automatique et les menus contextuels sont tous des popovers présentant différents styles mais rencontrant les mêmes trois problèmes techniques.
Un modal partage le problème de superposition des éléments, mais ce n’est pas un popover, car il bloque l’accès aux autres éléments. Tant qu’un modal est ouvert, le contenu derrière lui devient inactif : les utilisateurs ne peuvent ni y accéder avec la touche Tab, ni y cliquer, ni le faire défiler ; en général, un arrière-plan apparaît et le focus reste à l’intérieur du modal jusqu’à sa fermeture. Pensez à une demande de confirmation avant suppression : rien d’autre sur la page n’est utilisable tant que cette question n’a pas été répondue. Un popover, quant à lui, ne bloque rien ; la page reste entièrement interactive, et le popover se ferme simplement lorsque l’utilisateur passe à autre chose.
Les spécifications définissent directement cette distinction :
popover="auto"permet de fermer rapidement sans bloquer l’interface : il se ferme au clic à l’extérieur ou en appuyant sur Escape, laissant le focus libre. Les aides contextuelles et les menus déroulants relèvent de cette catégorie.popover="manual"reste ouvert jusqu’à ce que votre script le ferme, sans possibilité de fermeture rapide, ce qui convient à des notifications persistantes.- Un
<dialog>ouvert avec.showModal()est la variante bloquante : il occupe la couche supérieure, dispose d’un arrière-plan, conserve le focus et rend tout ce qui se trouve derrière lui inactif. - En revanche, l’ouverture de ce
<dialog>via.show()produit un élément non bloquant dont le comportement est très similaire à celui d’un popover.
Les inconvénients de l’approche JavaScript
Les tailles compressées ci-dessous proviennent de npm et ne couvrent que les bibliothèques elles-mêmes :
popper.js (v1, now deprecated) 7.1 KB
Tippy.js (bundles @popperjs/core) 14.1 KB
Floating UI, vanilla (@floating-ui/dom) 8.1 KB
Floating UI, React bindings 30.1 KB
react-tooltip (@floating-ui/dom + clsx) 14.1 KB
Ces chiffres excluent tout ce que l’on ajoute par-dessus : la configuration, le composant d’encapsulation, ainsi que les styles CSS pour les flèches et les thèmes. Il s’agit du coût de base avant toute logique personnalisée ; un projet qui combine, par exemple, un package d’indicateurs de texte avec un package de menu déroulant distinct doit le payer deux fois.
Rien dans tout cela ne reflète une mauvaise conception. L’interface flottante en particulier a été soigneusement développée ; sa fonction principale est de gérer correctement le comportement en cas de débordement dans tous les navigateurs, malgré leurs particularités. La dépense provient du fait qu’il a fallu résoudre trois problèmes non liés entre eux en JavaScript, car la plateforme ne proposait rien d’autre.
L’API Popover gère l’empilement et la fermeture
Deux spécifications distinctes ont remplacé cette bibliothèque, et elles ne répartissent pas les tâches comme on pourrait s’y attendre. L’attribut popover gère à la fois l’empilement et la fermeture, avec presque aucun script requis.
<button popovertarget="my-tooltip">Hover me</button>
<div id="my-tooltip" popover="auto">
This is the tooltip content.
</div>
L’attribut popovertarget relie le bouton à l’élément possédant un id correspondant. Avec popover="auto", l’élément est placé en premier plan dans le navigateur lorsqu’il s’ouvre, ce qui correspond précisément à la sortie du contexte de superposition overflow: hidden décrit précédemment, et il permet de le fermer facilement : des clics en dehors ou la touche Escape suffisent pour le fermer sans aucun code d’écouteur.
Une correction à la formulation du code : popovertarget s’active lors d’une action, c’est-à-dire un clic, une pression sur l’écran ou une touche du clavier, et non en survol. Une véritable aide contextuelle en survol nécessite néanmoins un petit script pour appeler showPopover() et hidePopover() lors des événements de pointeur et de focus, ou un mécanisme déclaratif plus récent une fois que les navigateurs cibles le prennent en charge. Malgré cela, l’empilement et la fermeture des popovers n’exigent plus de bibliothèque. La positionnement reste le seul point non résolu, et il relève d’une spécification différente.
Positionnement des ancrages : lier les éléments par nom
Le Positionnement des ancrages en CSS traite le seul problème que l’API Popover ignore. Il permet à n’importe quels deux éléments dans le document de se faire référence mutuellement par nom, plutôt que via une structure de parent et d’enfant.
.trigger {
anchor-name: --my-anchor;
}
.tooltip {
position: absolute;
position-anchor: --my-anchor;
top: anchor(--my-anchor bottom);
left: anchor(--my-anchor left);
}
anchor-name enregistre le déclencheur sous un identifiant barré, c’est la même syntaxe utilisée pour les propriétés personnalisées. position-anchor dans la tooltip fait référence à ce nom, et la fonction anchor() lit un bord spécifique de l’ancrage (top, right, bottom, left ou center) afin que la tooltip puisse s’aligner dessus.
Il est essentiel de noter que ces deux éléments n’ont pas besoin d’être contenus l’un dans l’autre. Le navigateur effectue désormais nativement ce que getBoundingClientRect() devait calculer manuellement à chaque mise à jour de défilement. Lorsque vous combinez cela avec un popover, rappelez-vous que le style feuille de style de l’agent utilisateur applique aux éléments [popover] les valeurs inset: 0 et margin: auto pour les centrer ; si la tooltip ignore vos offsets d’ancrage, réinitialiser ces propriétés est généralement la solution.
Déclenchement du retournement en cas de dépassement sans écouteur de défilement
La partie d’une bibliothèque de positionnement qui contient la majeure partie de sa logique concerne le traitement des dépassements : détecter que la tooltip est sur le point de sortir du champ de vision et choisir d’abord une autre position. La fonction de positionnement par ancrage gère cela grâce à position-try-fallbacks.
.tooltip {
position: absolute;
position-anchor: --my-anchor;
position-area: top center;
position-try-fallbacks: flip-block, flip-inline;
}
Ici, position-area: top center définit la position par défaut, et position-try-fallbacks énumère les alternatives que le navigateur essaie dans l’ordre lorsque cette position provoquerait un dépassement par rapport à son bloc contenant ou au champ de vision. flip-block reflète la tooltip le long de l’axe du bloc, ce qui fait que le haut devient le bas, tandis que flip-inline la reflète le long de l’axe en ligne, ce qui fait que la gauche devient la droite. Le navigateur réévalue cela lors du rendu, sans gestionnaire de défilement et sans script sur le thread principal pour détecter le dépassement.
Lorsqu’un miroir simple ne suffit pas, la règle @position-try vous permet de définir des emplacements de secours nommés, chacun étant un petit bloc de déclarations de positionnement que le navigateur peut parcourir successivement.
@position-try --below {
position-area: bottom center;
margin-top: 8px;
}
@position-try --above {
position-area: top center;
margin-bottom: 8px;
}
.tooltip {
position-anchor: --my-anchor;
position-try-fallbacks: --above, --below;
}
C’est la même décision que prend Floating UI en JavaScript à chaque événement de défilement, cette décision étant déclarée au préalable sous forme de données que le moteur de mise en page évalue. Notez que la règle .tooltip dans cet extrait suppose que l’élément est déjà positionné de manière absolue ou fixe, comme dans les exemples précédents ; la positionnement par ancre n’a aucun effet sur les éléments à positionnement statique.
Lorsqu’une bibliothèque de positionnement reste utile
Pour une boîte à outils, un menu déroulant simple ou une liste de complétion automatique, la combinaison native constitue désormais un choix par défaut judicieux. Le rôle de la bibliothèque s’est réduit, mais il n’a pas disparu pour autant.
La compatibilité des navigateurs constitue la première contrainte. Les navigateurs Chromium prennent en charge le positionnement des liens depuis la version 125, et @position-try a atteint le niveau de référence plus tard que anchor() lui-même. La compatibilité avec Safari et Firefox est arrivée par la suite, et les chiffres cités sur Internet varient ; il convient donc de consulter une table de compatibilité à jour plutôt que de se fier à une liste de versions unique. Lorsque la compatibilité fait défaut, il n’existe pas de dégradation propre au CSS : un navigateur qui ne comprend pas anchor() échoue simplement à positionner l’élément. Si vous devez prendre en charge des versions anciennes de Safari ou des navigateurs mobiles utilisant des moteurs obsolètes, prévoyez une solution de secours ; notre guide pour déployer du CSS moderne en toute sécurité aborde la détection des fonctionnalités et l’amélioration progressive pour les liens.
Le deuxième cas concerne des règles de placement qui vont au-delà du simple miroir en cas de débordement : un panneau flottant contenant une liste virtualisée, des collisions vérifiées simultanément par rapport à plusieurs limites, ou un placement déterminé par les données de l’application plutôt que par le layout. Un script peut réagir à n’importe quel état retenu par l’application, tandis qu’une liste de fallback CSS fixe ne prend en compte que le layout.
Pour le cas ordinaire, qui couvre la plupart des projets, il est difficile de justifier l’utilisation de 8 à 30 KB de JavaScript pour des tâches que le navigateur effectue désormais lui-même.
Points clés
- Un tooltip pose trois problèmes : l’empilement, la positionnement et le retrait. Des bibliothèques ont vu le jour parce que ces trois aspects devaient être résolus via du script.
popover="manual" ainsi que avec .showModal() couvrent les cas de permanence et d’obstruction.position-try-fallbacks combiné à @position-try remplace la logique de basculement en cas de dépassement qui dominait les exécutions des bibliothèques.