Présentation des principes de base sur la création d'un élément personnalisé d'info-bulle adaptatif et accessible aux couleurs.
Dans cet article, je souhaite partager mes réflexions sur la création d'un élément personnalisé <tool-tip>
adaptatif et accessible aux couleurs. Essayez la démonstration et consultez la source.
Si vous préférez la vidéo, voici une version YouTube de ce post:
Présentation
Une info-bulle est une superposition non interactive, non modale et non bloquante qui contient des informations supplémentaires sur les interfaces utilisateur. Il est masqué par défaut et apparaît lorsque l'utilisateur passe la souris sur un élément associé ou le sélectionne. Vous ne pouvez pas sélectionner une info-bulle ni interagir avec elle directement. Les info-bulles ne remplacent pas les étiquettes ou d'autres informations importantes. Un utilisateur doit pouvoir effectuer entièrement sa tâche sans info-bulle.
Info-bulle ou info-bulle
Comme pour de nombreux composants, il existe différentes descriptions de ce qu'est une info-bulle, par exemple dans MDN, WAI ARIA, Sarah Higley et Composants inclusifs. J'aime bien la séparation entre les info-bulles et les boutons d'activation/de désactivation. Une info-bulle doit contenir des informations supplémentaires non interactives, tandis qu'une info-bulle peut contenir des éléments interactifs et des informations importantes. La principale raison de la division est l'accessibilité, c'est-à-dire la manière dont les utilisateurs sont censés naviguer vers le pop-up et avoir accès aux informations et aux boutons qu'ils contiennent. Les boutons d'activation/de désactivation deviennent rapidement complexes.
Voici une vidéo d'un bouton d'activation du site Designcember, une superposition avec de l'interactivité qu'un utilisateur peut épingler pour ouvrir et explorer, puis fermer avec une légère fermeture ou la touche Échap:
Ce défi IUG a suivi la voie d'une info-bulle, cherchant à faire presque tout avec CSS. Voici comment le créer.
Markup
J'ai choisi d'utiliser un élément personnalisé <tool-tip>
. Les auteurs n'ont pas besoin de transformer des é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 un nom de classe moins spécifique. Aucun code JavaScript n'est impliqué.
<tool-tip>A tooltip</tool-tip>
C'est comme un élément div contenant du texte. Nous pouvons lier 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, cette info-bulle est désormais reconnue. Voyez dans l'exemple suivant comment le premier élément de lien possède un élément d'info-bulle reconnu dans son arborescence et que le second n'en a pas ? Le deuxième n'a pas ce rôle. Dans la section "Styles", nous allons améliorer cette arborescence.
Ensuite, nous avons besoin que l'info-bulle ne soit pas sélectionnable. Si un lecteur d'écran ne comprend pas le rôle d'info-bulle, les utilisateurs peuvent se concentrer sur <tool-tip>
pour en 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'est pas nécessaire de le cibler pour être accessible. Ici, nous pouvons utiliser inert
pour nous assurer qu'aucun utilisateur ne trouvera accidentellement le contenu de cette info-bulle dans leur 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>
supposent une position "supérieure", 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>
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 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>
dans 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-bulles, cette section est beaucoup plus simple. Tout d'abord, voyons quelle expérience utilisateur nous souhaitons:
- Dans les espaces restreints ou les interfaces surchargées, masquez les messages supplémentaires.
- Lorsqu'un utilisateur pointe sur un élément, le sélectionne ou utilise le toucher pour interagir avec un élément, affichez le message.
- Lorsque vous passez la souris, le curseur ou l'appui à la fin, le message est à nouveau masqué.
- Enfin, assurez-vous que tous les mouvements sont réduits si un utilisateur a spécifié une préférence pour les mouvements réduits.
Notre objectif est de proposer un service de messagerie complémentaire à la demande. Un utilisateur voyant une souris ou un clavier peut pointer dessus pour révéler le message et le lire avec ses yeux. Un utilisateur de lecteur d'écran non voyant peut se concentrer pour révéler le message, en le recevant audiblement via son outil.
Dans la section précédente, nous avons abordé l'arborescence d'accessibilité, le rôle d'info-bulle et "inert". Il ne reste plus qu'à la 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 facile de déterminer dans quelle partie du message audible une info-bulle. Vous pouvez également le voir lors du débogage dans l'arborescence d'accessibilité. Le texte du lien "top" est exécuté ensemble, sans hésitation, avec "Look, tooltips!" (Regardez, info-bulles). Le lecteur d'écran ne casse pas et n'identifie pas le texte en tant que contenu d'info-bulle.
Ajoutez un pseudo-élément de type lecteur d'écran à <tool-tip>
et nous pouvons ajouter notre propre texte de requête 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 pouvez voir 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 "Comporte une info-bulle: ".
Désormais, lorsqu'un utilisateur de lecteur d'écran sélectionne le lien, il indique "top" (haut) et fait une petite pause, puis annonce "has tooltip: look, tooltips" ( contient une info-bulle : regardez, info-bulles). Cela donne au lecteur d'écran quelques conseils intéressants sur l’expérience utilisateur. L'hésitation permet de séparer clairement le texte du lien et l'info-bulle. De plus, lorsqu'une info-bulle est annoncée, un utilisateur de lecteur d'écran peut facilement l'annuler s'il l'a déjà entendue. Cela vous rappelle de survoler et de relancer rapidement le point de la souris, comme vous l'avez déjà vu dans le message supplémentaire. C'était une belle parité UX.
Styles
L'élément <tool-tip>
sera un enfant de l'élément pour lequel il représente les 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 sur le contexte le plus proche, ce qui n'est pas ce que nous souhaitons. Un nouveau sélecteur sur le bloc peut vous aider, :has()
:
:has(> tool-tip) {
position: relative;
}
Ne vous préoccupez pas trop de la compatibilité du navigateur. Tout d'abord, rappelez-vous
que ces info-bulles sont complémentaires. Si elles ne fonctionnent pas,
cela ne devrait pas poser de problème. Ensuite, dans la section JavaScript, nous allons déployer un script pour émuler les fonctionnalités dont nous avons besoin pour les navigateurs non compatibles avec :has()
.
À présent, rendons les info-bulles non interactives pour qu'elles ne volent pas d'événements de pointeur à leur élément parent:
tool-tip {
…
pointer-events: none;
user-select: none;
}
Ensuite, masquez l'info-bulle avec l'option "Opacité" afin de pouvoir la faire passer avec un fondu enchaîné:
tool-tip {
opacity: 0;
}
:has(> tool-tip):is(:hover, :focus-visible, :active) > tool-tip {
opacity: 1;
}
:is()
et :has()
font le plus gros du travail, en faisant en sorte que tool-tip
contenant des éléments parents soit conscient de l'interactivité de l'utilisateur et permet d'activer/de désactiver la visibilité d'une info-bulle enfant. Les utilisateurs d'une souris peuvent pointer, les utilisateurs d'un clavier et d'un lecteur d'écran peuvent effectuer la mise au point, et les utilisateurs tactiles peuvent appuyer.
Maintenant que 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 l'endroit où nous en sommes, mais en ajoutant des ombres, une typographie et des couleurs pour ressembler à une 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
L'info-bulle n'a 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%;
}
}
Pour le thème clair, nous adaptons l'arrière-plan au blanc et réglons l'opacité des ombres dans une valeur bien moindre.
De droite à gauche
Pour prendre en charge les modes de lecture de droite à gauche, une propriété personnalisée stocke la valeur de l'orientation du document dans une valeur de -1 ou 1, respectivement.
tool-tip {
--isRTL: -1;
}
tool-tip:dir(rtl) {
--isRTL: 1;
}
Cela peut vous aider à positionner l'info-bulle:
tool-tip[tip-position="top"]) {
--_x: calc(50% * var(--isRTL));
}
Cette fonction vous assiste également à la position du 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 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 à la fois sa position physique et logique. Le code suivant montre comment chacune des quatre positions est stylisée 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 aux extrémités intégrées
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 début aligné
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
Pour le moment, nous n'avons modifié 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 de mouvement réduit généralement sûre. Nous animerons ensuite la position de transformation pour que l'info-bulle semble sortir de l'élément parent.
Une transition par défaut sûre et significative
Appliquez un style à l'élément d'info-bulle de sorte à modifier l'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, une info-bulle peut apparaître. Si l'utilisateur accepte le mouvement, positionnez légèrement la propriété translationX en lui appliquant une petite distance de déplacement depuis:
@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 qu'il s'agit du paramétrage de l'état "out", 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 devrait être requise pour accomplir une tâche dans votre interface utilisateur. Donc, si les info-bulles échouent complètement, ce n'est pas grave. Cela signifie également que nous pouvons considérer
les info-bulles comme progressivement améliorées. À terme, tous les navigateurs prendront en charge :has()
, et ce script peut disparaître complètement.
Le script de polyfill effectue deux actions, uniquement si le navigateur n'est pas compatible avec :has()
. Commencez par vérifier la compatibilité de :has()
:
if (!CSS.supports('selector(:has(*))')) {
// do work
}
Recherchez ensuite les éléments parents des <tool-tip>
et attribuez-leur un nom de classe avec lequel travailler:
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 des info-bulles si :has()
n'est pas compatible.
Conclusion
Maintenant que vous savez comment procéder, j'ai hâte d'utiliser l'API popup
pour faciliter l'utilisation des boutons d'activation, la couche supérieure pour éviter les problèmes liés au z-index et l'API anchor
pour mieux positionner les éléments dans la fenêtre. En attendant, je vais créer des info-bulles.
Diversissons nos approches et apprenons toutes les façons de créer sur le Web.
Créez une démo, cliquez sur les liens tweet me, et je l'ajouterai à la section "Remix" de la communauté ci-dessous.
Remix de la communauté
Rien à afficher pour le moment.
Ressources
- Code source sur GitHub