RGAA Lab

Composants accessibles

Le contenu au survol et au focus

Une infobulle, un aperçu, un sous-menu : tout ce qui apparaît au survol doit apparaître aussi au focus, se laisser survoler sans fuir, et se refermer par Échap. Démo, code, critères RGAA.

4 critères RGAA en jeu

Essayez-le

Un audit porte sur cent six critères, et son se lit en pourcentage.

Le contenu qui apparaît au survol relève du .

  • Infobulle : masqué
  • Aperçu : masqué

Survolez un déclencheur, puis descendez lentement la souris sur le contenu qui vient d’apparaître : il reste. Refaites-le au clavier — Tab jusqu’au déclencheur, le contenu s’affiche ; Tab encore, et vous entrez dans l’aperçu pour atteindre son lien. Puis le geste décisif : survolez un déclencheur sans rien toucher au clavier, et appuyez sur Échap — le contenu se retire alors que le pointeur n’a pas bougé, et la ligne d’état passe à « refermé par Échap ». Elle dit à chaque instant pourquoi le contenu reste ou non affiché.

Cette démo existe aussi en page autonome.

L’erreur courante

L’infobulle en CSS pur : un span masqué, révélé par le :hover de son parent.

<!-- À ne pas faire -->
<span class="astuce">
	taux de conformité
	<span class="bulle">Le rapport des critères conformes aux critères applicables.</span>
</span>
/* À ne pas faire : le survol est la seule porte d'entrée */
.astuce .bulle {
	display: none;
}
.astuce:hover .bulle {
	display: block;
}

Cette bulle n’existe que pour qui a une souris. Au clavier, rien ne se passe : le span déclencheur n’est pas focalisable, et lui ajouter :focus n’y changera rien — un span ne reçoit jamais le focus. Sur un écran tactile, il n’y a pas de survol du tout. Et l’explication perdue est presque toujours celle dont on avait le plus besoin, puisque c’est pour elle qu’on a écrit l’infobulle.

Le deuxième défaut est plus discret : même à la souris, la bulle disparaît dès que le pointeur la quitte. Impossible d’aller la lire à la loupe, d’en copier le texte, ou d’y cliquer si elle contient un lien.

Trois variantes du même problème :

  • Le sous-menu séparé de son libellé par un vide. Le pointeur descend, sort du libellé avant d’atteindre le panneau, et tout se referme sous lui. La rubrique reste inaccessible tant qu’on ne devine pas le trajet exact.
  • L’attribut title. Il ne s’affiche pas à la prise de focus clavier dans les navigateurs courants, ni au doigt, et son délai d’apparition ne se règle pas. Le référentiel le range en cas particulier du critère 10.13 — c’est le navigateur qui l’affiche, pas vous — ce qui le met hors du champ du critère sans le rendre recommandable pour autant.
  • La bulle qui se retire au bout de trois secondes. Elle disparaît pendant qu’on la lit, et c’est exactement ce que la troisième condition du critère 10.13 interdit.

Le code, pas à pas

1. Un déclencheur qui peut recevoir le focus

<button type="button" aria-describedby="bulle-taux">taux de conformité</button>

Tout part de là. Un contenu additionnel s’accroche à un <button> ou à un <a href> — quelque chose qui prend le focus par nature. C’est le seul moyen pour que le clavier ait un endroit où arriver.

Si le déclencheur doit rester du texte au fil d’une phrase, c’est le bouton qu’on habille : bordure pointillée, cursor: help, pas de fond. Ce qu’il ne faut pas faire, c’est poser tabindex="0" sur un span pour se dispenser du bouton — on obtiendrait un élément focalisable qui ne s’annonce comme rien.

2. Un groupe qui enveloppe le déclencheur et le contenu

<span class="groupe">
	<button type="button" aria-describedby="bulle-taux">taux de conformité</button>
	<span id="bulle-taux" role="tooltip" class="bulle">Le rapport des critères…</span>
</span>
.groupe .bulle {
	display: none;
}
.groupe:hover .bulle,
.groupe:focus-within .bulle {
	display: block;
}

Deux conditions du critère 10.13 se règlent en une seule ligne de CSS.

:hover sur le groupe, et non sur le déclencheur seul : le contenu est un descendant du groupe, donc amener le pointeur dessus ne fait pas sortir du groupe. C’est ce qui le rend survolable.

:focus-within fait le même travail pour le focus, et couvre le cas où le contenu renferme lui-même un lien : le focus est alors dans le groupe, le contenu reste affiché.

3. Le pont : le décalage appartient au contenu

.bulle {
	position: absolute;
	top: 100%;
	/* Transparent, mais à l'intérieur du groupe : le pointeur ne traverse rien. */
	padding-top: 0.5rem;
}

C’est le remède au sous-menu qui se ferme en chemin. Si l’écart entre le déclencheur et le panneau est une margin, il est hors de tout élément : le pointeur qui le traverse a quitté le groupe, et le panneau se referme. Le même écart écrit en padding transparent sur le contenu appartient au groupe, et le trajet devient continu.

La règle générale tient en une phrase : la zone survolable doit être d’un seul tenant, du déclencheur jusqu’au contenu.

4. Échap, en JavaScript

// Sur le document, pas sur le groupe : quand le contenu n'a été ouvert qu'au
// survol, le focus est ailleurs dans la page et le groupe ne verrait rien.
document.addEventListener('keydown', (evenement) => {
	if (evenement.key !== 'Escape' || !estAffiche()) return;
	groupe.classList.add('renvoye');
	// Le focus était dans le contenu qu'on vient de masquer : il n'a plus d'hôte.
	if (contenu.contains(document.activeElement)) declencheur.focus();
});

Le CSS sait afficher, il ne sait pas renoncer. La première condition du critère 10.13 — pouvoir masquer le contenu sans déplacer le focus ni le pointeur — demande donc un peu de script.

Et cet écouteur ne peut pas vivre sur le groupe. Un keydown ne remonte que depuis l’élément qui a le focus ; or la condition parle du pointeur autant que du focus, et le pointeur ne donne le focus à personne. Le contenu ouvert au seul survol laisse le focus ailleurs dans la page : la touche part de là, le groupe ne la voit jamais, et rien ne se referme. C’est le document qui écoute — c’est le seul endroit par où la touche passe dans les deux cas. En contrepartie, l’écouteur reçoit tous les Échap de la page : estAffiche() lui dit si ce groupe-ci a quelque chose à refermer, faute de quoi il capterait la touche au nez d’une boîte de dialogue ouverte ailleurs.

Trois points qui se ratent :

L’écouteur du document est le seul qui couvre le survol. Posé sur le groupe, il ne traite que la moitié du test 10.13.1 — celle où le contenu a été ouvert au focus.

Le refus ne vaut que pour la visite en cours. Une fois le pointeur et le focus sortis du groupe, la classe renvoye se retire, sans quoi un contenu refermé une fois ne reparaîtrait jamais.

Et si le focus se trouvait dans le contenu qu’on masque, il faut le rendre au déclencheur. Le laisser disparaître avec son hôte le renvoie au début de la page : c’est précisément ce que le test 7.3.2 interdit.

5. Du texte, ou des choses à cliquer ?

La réponse change le composant.

Pour du texte seul, role="tooltip" sur le contenu et aria-describedby du déclencheur vers lui. Une infobulle ne contient ni lien ni bouton : rien n’y est atteignable au clavier, et le motif ARIA ne le prévoit pas.

Dès qu’il y a quelque chose à cliquer — un aperçu avec un lien, un sous-menu — ce n’est plus une infobulle mais un dépliage : aria-expanded sur le déclencheur, aria-controls vers le contenu, et le contenu placé juste après le déclencheur dans l’ordre du DOM pour que la tabulation y entre naturellement. C’est là que joue le critère 12.11.

6. En avez-vous besoin ?

Un contenu additionnel est du contenu qu’on cache. Avant d’en écrire un, demandez-vous ce que coûterait de l’écrire dans la page : une définition sous le terme, une phrase d’aide sous le champ, une liste de rubriques dépliée. Rien à survoler, rien à refermer, rien à tester.

Le contenu additionnel se justifie quand il serait envahissant partout et utile par endroits. Pas quand il sert à faire tenir une page trop chargée.

Au clavier

ToucheEffet
TabAmène le focus au déclencheur, et le contenu apparaît.
Tab (encore)Entre dans le contenu s’il renferme un lien ou un bouton ; sinon passe au déclencheur suivant.
Maj + TabRessort du contenu vers le déclencheur, sans rien refermer.
ÉchapReferme le contenu, y compris quand il n’était ouvert qu’au survol. Le focus ne bouge pas — sauf s’il était dans le contenu refermé, où il revient au déclencheur.
Entrée / EspaceSur un déclencheur bouton : affiche ou masque le contenu, pour qui préfère ne pas dépendre du focus.

Ce que le lecteur d’écran restitue

Sur une infobulle : le nom, la nature, puis la description — de l’ordre de « taux de conformité, bouton, le rapport des critères conformes aux critères applicables ». Sur un aperçu dépliable : « critère 10.13, bouton, réduit », puis « développé » à l’ouverture. La formulation varie d’un lecteur à l’autre, les informations non.

Le diagnostic inverse est ici particulier. aria-describedby ne restitue rien tant que le contenu visé est masqué par display: none — et c’est normal. Si vous n’entendez pas la description, la cause la plus fréquente n’est pas l’attribut : c’est que le contenu n’apparaît pas à la prise de focus, donc reste masqué au moment où le lecteur voudrait le lire. C’est le critère 10.14 qui se manifeste sous les traits d’un problème d’ARIA.

Si vous entendez la description mais que Tab ne mène nulle part dans l’aperçu, le contenu n’est pas placé après le déclencheur dans le DOM, ou il se referme à la sortie du focus.

Checklist

  • Un déclencheur focalisable — un <button> ou un <a href>, jamais un span.
  • Le contenu affiché au survol et à la prise de focus, par le même groupe.
  • :hover et :focus-within posés sur le groupe, pas sur le déclencheur seul.
  • Aucun vide entre le déclencheur et le contenu : padding transparent, pas margin.
  • Échap referme, sans exiger de déplacer le pointeur ni le focus : l'écouteur est sur le document, pas sur le groupe.
  • role="tooltip" pour du texte seul ; aria-expanded dès qu’il y a à cliquer.
  • Le contenu placé juste après le déclencheur dans l’ordre du DOM.

Les critères RGAA en jeu

Vérifiez votre composant avec l'extension

Pour aller plus loin

Sources