Présentation de base de la création d'un élément personnalisé d'info-bulle accessible et adaptatif aux couleurs.
Dans cet article, je souhaite partager mes réflexions sur la façon de créer un élément personnalisé <tool-tip>
accessible et adaptatif aux couleurs. Essayez la démo et consultez la source.
Si vous préférez les vidéos, voici une version YouTube de cet article :
Présentation
Une info-bulle est une superposition non modale, non bloquante et non interactive contenant des informations supplémentaires pour les interfaces utilisateur. Il est masqué par défaut et devient visible lorsqu'un élément associé est pointé ou sélectionné. Il est impossible de sélectionner ou d'utiliser directement un info-bulle. Les info-bulles ne remplacent pas les libellés ni les autres informations importantes. Un utilisateur doit pouvoir accomplir entièrement sa tâche sans info-bulle.

À éviter : s'appuyer sur des info-bulles au lieu de libellés
Toggletip et info-bulle
Comme pour de nombreux composants, il existe différentes descriptions de ce qu'est un info-bulle, par exemple dans MDN, WAI-ARIA, Sarah Higley et Inclusive Components. J'aime la séparation entre les info-bulles et les info-boutons. Une info-bulle doit contenir des informations supplémentaires non interactives, tandis qu'une info-bouton peut contenir des informations importantes et interactives. La raison principale de cette division est l'accessibilité. Comment les utilisateurs sont-ils censés accéder au pop-up et aux informations et boutons qu'il contient ? Les info-bulles à bascule peuvent rapidement devenir complexes.
Voici une vidéo d'un info-bulle à bascule sur le site Designcember. Il s'agit d'un calque interactif qu'un utilisateur peut épingler et explorer, puis fermer en cliquant en dehors ou en appuyant sur la touche Échap :
Ce défi d'interface utilisateur a opté pour une info-bulle, en cherchant à tout faire avec CSS. Voici comment la créer.
Annoter
J'ai choisi d'utiliser un élément personnalisé <tool-tip>
. Les auteurs n'ont pas besoin de transformer les éléments personnalisés en composants Web s'ils ne le souhaitent pas. Le navigateur traitera <foo-bar>
comme un <div>
. Vous pouvez considérer un élément personnalisé comme une classe avec moins de spécificité. Aucun code JavaScript n'est impliqué.
<tool-tip>A tooltip</tool-tip>
C'est comme une div avec du texte à l'intérieur. Nous pouvons nous connecter à l'arborescence d'accessibilité des lecteurs d'écran compatibles en ajoutant [role="tooltip"]
.
<tool-tip role="tooltip">A tooltip</tool-tip>
Désormais, les lecteurs d'écran le reconnaissent comme une info-bulle. Dans l'exemple suivant, vous pouvez voir que le premier élément de lien comporte un élément d'info-bulle reconnu dans son arbre, contrairement au second. Le deuxième n'a pas le rôle. Dans la section des styles, nous allons améliorer cette arborescence.
Ensuite, nous devons faire en sorte que l'info-bulle ne soit pas sélectionnable. Si un lecteur d'écran ne comprend pas le rôle de l'info-bulle, il permettra aux utilisateurs de se concentrer sur <tool-tip>
pour lire le contenu, ce qui n'est pas nécessaire pour l'expérience utilisateur. Les lecteurs d'écran ajouteront le contenu à l'élément parent. Il n'est donc pas nécessaire de le rendre accessible en le sélectionnant. Ici, nous pouvons utiliser inert
pour nous assurer qu'aucun utilisateur ne trouvera accidentellement le contenu de cet info-bulle dans le flux d'onglets :
<tool-tip inert role="tooltip">A tooltip</tool-tip>
J'ai ensuite choisi d'utiliser des attributs comme interface pour spécifier la position de l'info-bulle. Par défaut, tous les <tool-tip>
s sont positionnés en haut de la page, mais vous pouvez personnaliser la position d'un élément en ajoutant tip-position
:
<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>
J'ai tendance à utiliser des attributs plutôt que des classes pour ce genre de choses, afin que <tool-tip>
ne puisse pas avoir plusieurs positions attribuées en même temps.
Il ne peut y en avoir qu'un ou aucun.
Enfin, placez les éléments <tool-tip>
à l'intérieur de l'élément pour lequel vous souhaitez fournir une info-bulle. Ici, je partage le texte alt
avec les utilisateurs voyants en plaçant une image et un <tool-tip>
à l'intérieur d'un élément <picture>
:
<picture>
<img alt="The GUI Challenges skull logo" width="100" src="...">
<tool-tip role="tooltip" tip-position="bottom">
The <b>GUI Challenges</b> skull logo
</tool-tip>
</picture>
Ici, je place un <tool-tip>
à l'intérieur d'un élément <abbr>
:
<p>
The <abbr>HTML <tool-tip role="tooltip" tip-position="top">Hyper Text Markup Language</tool-tip></abbr> abbr element.
</p>
Accessibilité
Comme j'ai choisi de créer des info-bulles et non des info-bascules, cette section est beaucoup plus simple. Tout d'abord, laissez-moi vous présenter l'expérience utilisateur que nous souhaitons proposer :
- Dans les espaces restreints ou les interfaces encombrées, masquez les messages supplémentaires.
- Lorsque l'utilisateur pointe sur un élément, le sélectionne ou interagit avec lui par le biais d'une commande tactile, affichez le message.
- Lorsque le pointeur, la sélection ou le doigt ne sont plus sur le message, masquez-le à nouveau.
- Enfin, assurez-vous de réduire tout mouvement si un utilisateur a spécifié une préférence pour les mouvements réduits.
Notre objectif est de proposer des messages supplémentaires à la demande. Un utilisateur voyant qui utilise une souris ou un clavier peut pointer sur le message pour l'afficher et le lire. Un utilisateur de lecteur d'écran non voyant peut sélectionner le message pour le révéler et l'entendre grâce à son outil.

Dans la section précédente, nous avons abordé l'arborescence d'accessibilité, le rôle de l'info-bulle et l'inertie. Il ne reste plus qu'à tester et vérifier que l'expérience utilisateur révèle correctement le message de l'info-bulle à l'utilisateur. Lors des tests, il n'est pas clair quelle partie du message audio est une info-bulle. Cela peut également être observé lors du débogage dans l'arborescence d'accessibilité, où le texte du lien "top" est exécuté sans hésitation avec "Look, tooltips!". Le lecteur d'écran ne segmente pas le texte ni ne l'identifie comme contenu d'info-bulle.
Ajoutez un pseudo-élément réservé aux lecteurs d'écran à <tool-tip>
pour ajouter votre propre texte d'invite pour les utilisateurs non voyants.
&::before {
content: "; Has tooltip: ";
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
Vous trouverez ci-dessous l'arborescence d'accessibilité mise à jour, qui comporte désormais un point-virgule après le texte du lien et une invite pour l'info-bulle "Has tooltip: ".
Désormais, lorsqu'un utilisateur de lecteur d'écran sélectionne le lien, le lecteur énonce "en haut", fait une petite pause, puis annonce "a un info-bulle : regardez, des info-bulles". Cela fournit à l'utilisateur du lecteur d'écran quelques conseils utiles en termes d'UX. L'hésitation permet de bien séparer le texte du lien et l'info-bulle. De plus, lorsque l'option "a un info-bulle" est annoncée, un utilisateur de lecteur d'écran peut facilement l'annuler s'il l'a déjà entendue. Cela ressemble beaucoup à l'action de pointer rapidement sur un élément et de le quitter, car vous avez déjà vu le message supplémentaire. Cela semblait être une bonne parité UX.
Styles
L'élément <tool-tip>
sera un enfant de l'élément pour lequel il représente un message supplémentaire. Commençons donc par les éléments essentiels de l'effet de superposition. Sortez-le du flux de documents avec position absolute
:
tool-tip {
position: absolute;
z-index: 1;
}
Si le parent n'est pas un contexte d'empilement, l'info-bulle se positionne sur le contexte d'empilement le plus proche, ce qui n'est pas ce que nous voulons. Un nouveau sélecteur peut vous aider : :has()
.
:has(> tool-tip) {
position: relative;
}
Ne vous inquiétez pas trop de la compatibilité avec les navigateurs. Tout d'abord, n'oubliez pas que ces info-bulles sont complémentaires. Si elles ne fonctionnent pas, tout devrait bien se passer. Ensuite, dans la section JavaScript, nous allons déployer un script pour polyfiller la fonctionnalité dont nous avons besoin pour les navigateurs sans prise en charge de :has()
.
Ensuite, rendons les info-bulles non interactives afin qu'elles ne volent pas les événements de pointeur de leur élément parent :
tool-tip {
…
pointer-events: none;
user-select: none;
}
Ensuite, masquez l'info-bulle avec de l'opacité afin de pouvoir la faire disparaître en fondu :
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
:is()
et :has()
font le gros du travail ici, en informant tool-tip
contenant des éléments parents de l'interactivité de l'utilisateur pour activer/désactiver la visibilité d'un info-bulle enfant. Les utilisateurs de souris peuvent pointer, ceux de clavier et de lecteur d'écran peuvent sélectionner, et ceux d'écran tactile peuvent appuyer.
Maintenant que l'affichage et le masquage de la superposition fonctionnent pour les utilisateurs voyants, il est temps d'ajouter des styles pour la thématisation, le positionnement et l'ajout de la forme triangulaire à la bulle. Les styles suivants commencent à utiliser des propriétés personnalisées, en s'appuyant sur ce que nous avons fait jusqu'à présent, mais en ajoutant également des ombres, de la typographie et des couleurs pour qu'il ressemble à un info-bulle flottante :
tool-tip {
--_p-inline: 1.5ch;
--_p-block: .75ch;
--_triangle-size: 7px;
--_bg: hsl(0 0% 20%);
--_shadow-alpha: 50%;
--_bottom-tip: conic-gradient(from -30deg at bottom, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) bottom / 100% 50% no-repeat;
--_top-tip: conic-gradient(from 150deg at top, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) top / 100% 50% no-repeat;
--_right-tip: conic-gradient(from -120deg at right, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) right / 50% 100% no-repeat;
--_left-tip: conic-gradient(from 60deg at left, rgba(0,0,0,0), #000 1deg 60deg, rgba(0,0,0,0) 61deg) left / 50% 100% no-repeat;
pointer-events: none;
user-select: none;
opacity: 0;
transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
transition: opacity .2s ease, transform .2s ease;
position: absolute;
z-index: 1;
inline-size: max-content;
max-inline-size: 25ch;
text-align: start;
font-size: 1rem;
font-weight: normal;
line-height: normal;
line-height: initial;
padding: var(--_p-block) var(--_p-inline);
margin: 0;
border-radius: 5px;
background: var(--_bg);
color: CanvasText;
will-change: filter;
filter:
drop-shadow(0 3px 3px hsl(0 0% 0% / var(--_shadow-alpha)))
drop-shadow(0 12px 12px hsl(0 0% 0% / var(--_shadow-alpha)));
}
/* create a stacking context for elements with > tool-tips */
:has(> tool-tip) {
position: relative;
}
/* when those parent elements have focus, hover, etc */
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
transition-delay: 200ms;
}
/* prepend some prose for screen readers only */
tool-tip::before {
content: "; Has tooltip: ";
clip: rect(1px, 1px, 1px, 1px);
clip-path: inset(50%);
height: 1px;
width: 1px;
margin: -1px;
overflow: hidden;
padding: 0;
position: absolute;
}
/* tooltip shape is a pseudo element so we can cast a shadow */
tool-tip::after {
content: "";
background: var(--_bg);
position: absolute;
z-index: -1;
inset: 0;
mask: var(--_tip);
}
/* top tooltip styles */
tool-tip:is(
[tip-position="top"],
[tip-position="block-start"],
:not([tip-position]),
[tip-position="bottom"],
[tip-position="block-end"]
) {
text-align: center;
}
Ajustements du thème
La info-bulle ne comporte que quelques couleurs à gérer, car la couleur du texte est héritée de la page via le mot clé système CanvasText
. De plus, comme nous avons créé des propriétés personnalisées pour stocker les valeurs, nous pouvons mettre à jour uniquement ces propriétés personnalisées et laisser le thème gérer le reste :
@media (prefers-color-scheme: light) {
tool-tip {
--_bg: white;
--_shadow-alpha: 15%;
}
}
Pour le thème clair, nous adaptons l'arrière-plan en le rendant blanc et nous atténuons considérablement les ombres en ajustant leur opacité.
De droite à gauche
Pour prendre en charge les modes de lecture de droite à gauche, une propriété personnalisée stocke la valeur de la direction du document dans une valeur de -1 ou 1, respectivement.
tool-tip {
--isRTL: -1;
}
tool-tip:dir(rtl) {
--isRTL: 1;
}
Cela peut être utilisé pour vous aider à positionner l'info-bulle :
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
et vous aider à localiser le triangle :
tool-tip[tip-position="right"]::after {
--_tip: var(--_left-tip);
}
tool-tip[tip-position="right"]:dir(rtl)::after {
--_tip: var(--_right-tip);
}
Enfin, peut également être utilisé pour les transformations logiques sur translateX()
:
--_x: calc(var(--isRTL) * -3px * -1);
Positionnement de l'info-bulle
Positionnez l'info-bulle de manière logique avec les propriétés inset-block
ou inset-inline
pour gérer les positions physiques et logiques de l'info-bulle. Le code suivant montre comment chacun des quatre emplacements est stylisé pour les directions de gauche à droite et de droite à gauche.
Alignement en haut et au début du bloc
tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position])) {
inset-inline-start: 50%;
inset-block-end: calc(100% + var(--_p-block) + var(--_triangle-size));
--_x: calc(50% * var(--isRTL));
}
tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))::after {
--_tip: var(--_bottom-tip);
inset-block-end: calc(var(--_triangle-size) * -1);
border-block-end: var(--_triangle-size) solid transparent;
}
Alignement à droite et à la fin de la ligne
tool-tip:is([tip-position="right"], [tip-position="inline-end"]) {
inset-inline-start: calc(100% + var(--_p-inline) + var(--_triangle-size));
inset-block-end: 50%;
--_y: 50%;
}
tool-tip:is([tip-position="right"], [tip-position="inline-end"])::after {
--_tip: var(--_left-tip);
inset-inline-start: calc(var(--_triangle-size) * -1);
border-inline-start: var(--_triangle-size) solid transparent;
}
tool-tip:is([tip-position="right"], [tip-position="inline-end"]):dir(rtl)::after {
--_tip: var(--_right-tip);
}
Alignement en bas et en fin de bloc
tool-tip:is([tip-position="bottom"], [tip-position="block-end"]) {
inset-inline-start: 50%;
inset-block-start: calc(100% + var(--_p-block) + var(--_triangle-size));
--_x: calc(50% * var(--isRTL));
}
tool-tip:is([tip-position="bottom"], [tip-position="block-end"])::after {
--_tip: var(--_top-tip);
inset-block-start: calc(var(--_triangle-size) * -1);
border-block-start: var(--_triangle-size) solid transparent;
}
Alignement à gauche et au début de la ligne
tool-tip:is([tip-position="left"], [tip-position="inline-start"]) {
inset-inline-end: calc(100% + var(--_p-inline) + var(--_triangle-size));
inset-block-end: 50%;
--_y: 50%;
}
tool-tip:is([tip-position="left"], [tip-position="inline-start"])::after {
--_tip: var(--_right-tip);
inset-inline-end: calc(var(--_triangle-size) * -1);
border-inline-end: var(--_triangle-size) solid transparent;
}
tool-tip:is([tip-position="left"], [tip-position="inline-start"]):dir(rtl)::after {
--_tip: var(--_left-tip);
}
Animation
Jusqu'à présent, nous n'avons fait que modifier la visibilité de l'info-bulle. Dans cette section, nous allons d'abord animer l'opacité pour tous les utilisateurs, car il s'agit d'une transition à mouvement réduit généralement sûre. Nous allons ensuite animer la position de transformation pour que l'info-bulle semble glisser hors de l'élément parent.
Une transition par défaut sûre et pertinente
Définissez le style de l'élément d'info-bulle pour la transition d'opacité et la transformation, comme suit :
tool-tip {
opacity: 0;
transform: translateX(var(--_x, 0)) translateY(var(--_y, 0));
transition: opacity .2s ease, transform .2s ease;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
transition-delay: 200ms;
}
Ajouter du mouvement à la transition
Pour chacun des côtés sur lesquels un info-bulle peut apparaître, si l'utilisateur accepte le mouvement, positionnez légèrement la propriété translateX en lui donnant une petite distance à parcourir :
@media (prefers-reduced-motion: no-preference) {
:has(> tool-tip:is([tip-position="top"], [tip-position="block-start"], :not([tip-position]))):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_y: 3px;
}
:has(> tool-tip:is([tip-position="right"], [tip-position="inline-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_x: -3px;
}
:has(> tool-tip:is([tip-position="bottom"], [tip-position="block-end"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_y: -3px;
}
:has(> tool-tip:is([tip-position="left"], [tip-position="inline-start"])):not(:hover):not(:focus-visible):not(:active) tool-tip {
--_x: 3px;
}
}
Notez que l'état "out" est défini, car l'état "in" est à translateX(0)
.
JavaScript
À mon avis, JavaScript est facultatif. En effet, aucune de ces info-bulles ne doit être obligatoire pour accomplir une tâche dans votre UI. Par conséquent, si les info-bulles ne fonctionnent pas du tout, cela ne devrait pas poser de problème. Cela signifie également que nous pouvons traiter les info-bulles comme des éléments améliorés progressivement. À terme, tous les navigateurs seront compatibles avec :has()
et ce script pourra disparaître complètement.
Le script polyfill effectue deux actions, et ce uniquement si le navigateur n'est pas compatible avec :has()
. Tout d'abord, vérifiez la compatibilité avec :has()
:
if (!CSS.supports('selector(:has(*))')) {
// do work
}
Ensuite, recherchez les éléments parents des <tool-tip>
et attribuez-leur un nom de classe pour travailler avec :
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
}
Ensuite, injectez un ensemble de styles qui utilisent ce nom de classe, en simulant le sélecteur :has()
pour le même comportement :
if (!CSS.supports('selector(:has(*))')) {
document.querySelectorAll('tool-tip').forEach(tooltip =>
tooltip.parentNode.classList.add('has_tool-tip'))
let styles = document.createElement('style')
styles.textContent = `
.has_tool-tip {
position: relative;
}
.has_tool-tip:is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
transition-delay: 200ms;
}
`
document.head.appendChild(styles)
}
Voilà, tous les navigateurs afficheront désormais les info-bulles si :has()
n'est pas pris en charge.
Conclusion
Maintenant que vous savez comment j'ai fait, comment feriez-vous ? 🙂 J'ai vraiment hâte de découvrir l'API popup
pour faciliter les info-bulles, la couche supérieure pour éviter les conflits d'index Z et l'API anchor
pour mieux positionner les éléments dans la fenêtre. En attendant, je vais créer des info-bulles.
Diversifions nos approches et découvrons toutes les façons de créer sur le Web.
Créez une démo, tweetez-moi les liens et je l'ajouterai à la section des remix de la communauté ci-dessous !
Remix de la communauté
Aucun élément à afficher pour le moment.
Ressources
- Code source sur GitHub