Créer un composant d'info-bulle

Présentation de base sur la création d'un élément d'info-bulle personnalisé accessible et adaptatif aux couleurs.

Dans ce post, j'aimerais vous expliquer comment créer un élément personnalisé <tool-tip> adaptable aux couleurs et accessible. Essayez la démonstration et consultez le code source.

Une info-bulle est présentée dans différents exemples et schémas de couleurs.

Si vous préférez regarder une vidéo, 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 sur les interfaces utilisateur. Il est masqué par défaut et n'est plus masqué lorsqu'un élément associé est pointé ou sélectionné. Une info-bulle ne peut pas être sélectionnée ni utilisée directement. Les info-bulles ne remplacent pas les libellés ni d'autres informations de grande valeur. Un utilisateur doit pouvoir effectuer sa tâche sans info-bulle.

À faire : toujours étiqueter vos entrées.
Ne pas : utiliser des info-bulles à la place de libellés

Toggletip et info-bulle

Comme de nombreux composants, il existe différentes descriptions de ce qu'est une info-bulle, par exemple dans MDN, WAI ARIA, Sarah Higley et InclusiveComponents. J'aime la séparation entre les info-bulles et les info-bulles. Une info-bulle doit contenir des informations supplémentaires non interactives, tandis qu'une info-bulle à bouton d'activation/de désactivation peut contenir de l'interactivité et des informations importantes. La principale raison de cette division est l'accessibilité. Comment les utilisateurs doivent-ils accéder au pop-up et avoir accès aux informations et aux boutons qu'il contient ? Les info-bulles à bascule deviennent rapidement complexes.

Voici une vidéo d'un bouton d'activation/de désactivation du site Designcember. Il s'agit d'un calque interactif qu'un utilisateur peut épingler et explorer, puis fermer avec la touche de suppression ou la touche Échap :

Ce défi d'IUG a pris la voie d'une info-bulle, visant à faire presque tout 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 d'ajouter d'éléments personnalisés dans des composants Web s'ils ne le souhaitent pas. Le navigateur traitera <foo-bar> comme un <div>. Vous pourriez considérer un élément personnalisé comme un nom de classe avec moins de spécificité. Aucun code JavaScript n'est impliqué.

<tool-tip>A tooltip</tool-tip>

Il s'agit d'un élément div contenant du texte. Nous pouvons nous appuyer sur l'arborescence d'accessibilité des lecteurs d'écran compatibles en ajoutant [role="tooltip"].

<tool-tip role="tooltip">A tooltip</tool-tip>

Pour les lecteurs d'écran, il s'agit maintenant d'une info-bulle. Dans l'exemple suivant, voyez comment le premier élément de lien comporte un élément d'info-bulle reconnu dans son arborescence, contrairement au second. Le second n'a pas le rôle. Dans la section "Styles", nous allons améliorer cette vue arborescente.

Capture d&#39;écran de l&#39;arborescence d&#39;accessibilité des outils pour les développeurs Chrome représentant le code HTML. Affiche un lien avec le texte &quot;top ; Has tooltip: Hey, a tooltip!&quot; (En haut ; contient une info-bulle : Hey, une info-bulle !) que vous pouvez sélectionner. Il contient du texte statique &quot;top&quot; et un élément d&#39;info-bulle.

Ensuite, nous devons empêcher la bulle d'info de pouvoir être sélectionnée. Si un lecteur d'écran ne comprend pas le rôle de l'info-bulle, il permettra aux utilisateurs de sélectionner <tool-tip> pour lire le contenu, ce qui n'est pas nécessaire pour l'expérience utilisateur. Les lecteurs d'écran ajoutent le contenu à l'élément parent. Par conséquent, il n'a pas besoin d'être sélectionné pour être accessible. Ici, nous pouvons utiliser inert pour nous assurer qu'aucun utilisateur ne trouvera accidentellement le contenu de cette info-bulle dans le flux de leur onglet:

<tool-tip inert role="tooltip">A tooltip</tool-tip>

Autre capture d&#39;écran de l&#39;arborescence d&#39;accessibilité de Chrome DevTools. Cette fois, l&#39;élément d&#39;info-bulle est manquant.

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> occupent une position "haut", mais la position peut être personnalisée sur un élément en ajoutant tip-position :

<tool-tip role="tooltip" tip-position="right ">A tooltip</tool-tip>

Capture d&#39;écran d&#39;un lien avec une info-bulle à droite indiquant &quot;Une info-bulle&quot;.

Pour ce type d'opérations, j'ai tendance à utiliser des attributs plutôt que des classes, afin que <tool-tip> ne puisse pas avoir plusieurs positions en même temps. Il ne peut y en avoir qu'un seul ou aucun.

Enfin, placez des éléments <tool-tip> dans l'élément pour lequel vous souhaitez fournir une info-bulle. Je partage ici 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>

Capture d&#39;écran d&#39;une image avec une info-bulle indiquant &quot;Logo du crâne des défis GUI&quot;.

Je place ici 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>

Capture d&#39;écran d&#39;un paragraphe avec l&#39;acronyme HTML souligné et une info-bulle au-dessus indiquant &quot;Hyper Text Markup Language&quot;.

Accessibilité

Étant donné que j'ai choisi de créer des info-bulles et non des info-bulles, cette section est beaucoup plus simple. Tout d'abord, laissez-moi décrire l'expérience utilisateur souhaitée :

  1. Dans les espaces restreints ou les interfaces surchargées, masquez les messages supplémentaires.
  2. Lorsque l'utilisateur pointe, place son curseur ou utilise l'écran tactile pour interagir avec un élément, le message s'affiche.
  3. Masquez à nouveau le message lorsque vous passez le curseur ou appuyez dessus.
  4. Enfin, assurez-vous que tout mouvement est réduit si un utilisateur a spécifié une préférence pour un mouvement réduit.

Notre objectif est de proposer des messages complémentaires à la demande. Un utilisateur voyant qui utilise une souris ou un clavier peut pointer sur le message pour le révéler et le lire. Un utilisateur non voyant peut afficher le message et le recevoir par le biais de son outil.

Screenshot of MacOS VoiceOver reading a link with a tooltip

Dans la section précédente, nous avons abordé l'arborescence d'accessibilité, le rôle de l'info-bulle et l'inactivité. Il ne reste plus qu'à le tester et à vérifier que l'expérience utilisateur révèle correctement le message de l'info-bulle. Lors des tests, il n'est pas facile de savoir quelle partie du message audible correspond à une info-bulle. Il est également visible lors du débogage dans l'arborescence d'accessibilité. Le texte du lien "top" est exécuté ensemble, sans hésitation, avec "Look, tooltips!". Le lecteur d'écran ne scinde pas le texte ni ne l'identifie comme contenu d'info-bulle.

Capture d&#39;écran de l&#39;arborescence d&#39;accessibilité des outils pour les développeurs Chrome, où le texte du lien indique &quot;top Hey, a tooltip!&quot; (Haut. Hé, une info-bulle !)

Ajoutez un pseudo-élément réservé aux lecteurs d'écran à <tool-tip>. Nous pouvons ainsi ajouter notre 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 "A une info-bulle : ".

Capture d&#39;écran mise à jour de l&#39;arborescence d&#39;accessibilité des outils pour les développeurs Chrome où le texte du lien a amélioré la formulation, &quot;top ; Has tooltip: Hey, a tooltip!

Désormais, lorsqu'un utilisateur de lecteur d'écran sélectionne le lien, il entend "top" (en haut) et fait une petite pause, puis annonce "has tooltip: look, tooltips" (a une info-bulle : regardez, info-bulles). Cela donne à l'utilisateur d'un lecteur d'écran quelques conseils d'UX. L'hésitation permet de bien séparer le texte du lien de la bulle d'info. De plus, lorsque "a tooltip" est annoncé, un utilisateur de lecteur d'écran peut facilement l'annuler s'il l'a déjà entendu auparavant. Cela rappelle beaucoup le survol et le désélectionnement rapide, comme vous l'avez déjà vu dans le message supplémentaire. Cela ressemblait à une belle parité UX.

Styles

L'élément <tool-tip> sera un enfant de l'élément pour lequel il représente des messages supplémentaires. Commençons donc par les éléments essentiels de l'effet de superposition. Retirez-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 dans le contexte le plus proche, ce qui n'est pas ce que nous voulons. Un nouveau sélecteur dans le bloc peut vous aider, :has():

Navigateurs pris en charge

  • Chrome : 105
  • Edge: 105
  • Firefox: 121
  • Safari : 15.4.

Source

:has(> tool-tip) {
  position: relative;
}

Ne vous préoccupez 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, ce n'est pas un problème. Deuxièmement, dans la section JavaScript, nous allons déployer un script pour polyfiller la fonctionnalité dont nous avons besoin pour les navigateurs qui ne sont pas compatibles avec :has().

Ensuite, faisons en sorte que les info-bulles ne soient pas interactives afin qu'elles ne volent pas les événements de pointeur à leur élément parent :

tool-tip {
  
  pointer-events: none;
  user-select: none;
}

Ensuite, masquez l'info-bulle avec l'opacité afin de pouvoir la faire passer à l'aide d'un fondu croisé :

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 faisant en sorte que tool-tip contenant des éléments parents soit conscient de l'interactivité de l'utilisateur pour activer/désactiver la visibilité d'une info-bulle enfant. Les utilisateurs de la souris peuvent pointer dessus, les utilisateurs de clavier et de lecteur d'écran peuvent la sélectionner, et les utilisateurs tactiles peuvent appuyer dessus.

Comme la superposition d'affichage et de masquage fonctionne 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 à une info-bulle flottante :

Capture d&#39;écran de l&#39;info-bulle en mode sombre, flottant au-dessus du lien &quot;block-start&quot;.

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

L'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 ne pouvons mettre à jour que 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%;
  }
}

Capture d&#39;écran côte à côte des versions claire et sombre de l&#39;info-bulle.

Pour le thème clair, nous adaptons l'arrière-plan au blanc et atténuons 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;
}

Vous pouvez utiliser cette propriété pour vous aider à positionner l'info-bulle:

tool-tip[tip-position="top"]) {
  --_x: calc(50% * var(--isRTL));
}

et vous indiquer où se trouve 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, il peut également être utilisé pour les transformations logiques sur translateX() :

--_x: calc(var(--isRTL) * -3px * -1);

Positionnement des info-bulles

Positionnez l'info-bulle de manière logique avec les propriétés inset-block ou inset-inline pour gérer à la fois les positions physiques et logiques de l'info-bulle. Le code suivant montre comment chacune des quatre positions est stylisée pour les directions de lecture de gauche à droite et de droite à gauche.

Alignement en haut et au début du bloc

Capture d&#39;écran montrant la différence de positionnement entre la position supérieure de gauche à droite et la position supérieure de droite à gauche.

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 en ligne

Capture d&#39;écran montrant la différence de positionnement entre la position de droite à gauche et la position de fin en ligne de droite à gauche.

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

Capture d&#39;écran montrant la différence de positionnement entre la position inférieure de gauche à droite et la position de fin de bloc de droite à gauche.

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 en ligne de début

Capture d&#39;écran montrant la différence de positionnement entre la position de gauche à droite et la position de début en ligne de droite à gauche.

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 activé que 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

Modifiez le style de l'élément d'info-bulle pour effectuer une transition d'opacité et une 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 une 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 à partir de:

@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 défini sur translateX(0).

JavaScript

À mon avis, le code 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 échouent complètement, cela ne devrait pas poser de problème. Cela signifie également que nous pouvons traiter les info-bulles comme des améliorations progressives. À terme, tous les navigateurs seront compatibles avec :has(), et ce script pourra disparaître complètement.

Le script de polyfill effectue deux actions, et ne le fait que si le navigateur n'est pas compatible avec :has(). Commencez par vérifier si :has() est compatible:

if (!CSS.supports('selector(:has(*))')) {
  // do work
}

Recherchez ensuite les éléments parents des <tool-tip> et attribuez-leur un nom de classe à utiliser :

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 cette classe de nom, en simulant le sélecteur :has() pour obtenir exactement 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)
}

C'est tout. Désormais, tous les navigateurs affichent les info-bulles si :has() n'est pas compatible.

Conclusion

Maintenant que vous savez comment j'ai fait, comment pourriez-vous procéder ? 🙂 J'attends avec impatience l'API popup pour faciliter les boutons d'activation/de désactivation, la couche supérieure pour éviter les batailles d'indice 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, envoyez-moi des tweets via des liens et je l'ajouterai à la section "Remix de la communauté" ci-dessous.

Remix de la communauté

Aucun élément à afficher pour le moment.

Ressources