Créer un composant d'info-bulle

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.

Une info-bulle s'affiche et fonctionne dans différents exemples et schémas de couleurs.

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.

À faire : étiquetez toujours vos entrées.
À é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.

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; qui est focusable. Il contient du texte statique &quot;top&quot; et un élément d&#39;info-bulle.

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>

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

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

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>

Capture d&#39;écran d&#39;une image avec un info-bulle indiquant &quot;Logo de la tête de mort des défis de l&#39;interface utilisateur graphique&quot;.

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>

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

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 :

  1. Dans les espaces restreints ou les interfaces encombrées, masquez les messages supplémentaires.
  2. 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.
  3. Lorsque le pointeur, la sélection ou le doigt ne sont plus sur le message, masquez-le à nouveau.
  4. 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.

Capture d'écran de VoiceOver sur macOS lisant un lien avec un info-bulle

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.

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; (en haut, Hé, une 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: ".

Capture d&#39;écran mise à jour de l&#39;arborescence d&#39;accessibilité des outils de développement Chrome, où le texte du lien a été reformulé : &quot;top ; Has tooltip: Hey, a tooltip!&quot; (en haut ; avec info-bulle : Hé, une info-bulle !).

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().

Browser Support

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

Source

: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 :

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

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%;
  }
}

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

Capture d&#39;écran montrant la différence de placement 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 à la fin de la ligne

Capture d&#39;écran montrant la différence de placement entre la position de droite de gauche à droite et la position de fin de 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 placement entre la position de bas en haut (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 au début de la ligne

Capture d&#39;écran montrant la différence de placement entre la position de gauche de gauche à droite et la position de début de 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 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