Barre d'outils accessible : role toolbar et roving tabindex
Implémenter role=toolbar avec un vrai roving tabindex : un seul arrêt de tabulation, navigation aux flèches, nom accessible, aria-pressed et critères RGAA à vérifier.
La barre d'outils (toolbar) est un conteneur qui regroupe des contrôles (boutons, boutons de menu, cases à cocher) et transforme une longue rangée d'arrêts de tabulation en un seul : la navigation interne se fait aux flèches. C'est toute la raison d'être de role="toolbar", et elle repose sur une technique précise, le roving tabindex : exactement un contrôle porte tabindex="0", tous les autres tabindex="-1", et cet attribut suit le dernier contrôle utilisé.
Il n'existe pas d'élément HTML natif pour ce motif. Et sans le bénéfice de l'arrêt de tabulation unique, de simples <button> sans rôle toolbar sont préférables : le rôle sans le comportement est une promesse rompue.
Quand ce motif est le mauvais choix
- Deux ou trois boutons : rien à économiser. Le rôle
toolbarimpose à l'utilisateur de connaître le modèle aux flèches pour aucun gain ; des boutons simples suffisent. - Une rangée de liens : c'est de la navigation, donc
<nav>+<ul>+<a>, jamais une barre d'outils. - Un composant qui consomme lui-même les flèches, comme une listbox ou une combobox, placé directement dans la barre : les gestions de flèches entrent en collision. L'envelopper dans un bouton de menu, ou repenser la structure.
- Une barre riche d'un éditeur avec de nombreux contrôles : c'est le cas nominal du motif toolbar, à condition d'implémenter réellement le roving tabindex.
Critères RGAA applicables
- Critère 7.1 : compatibilité des scripts avec les technologies d'assistance. Le rôle
toolbaret son nom accessible doivent être exposés ; deux barres d'outils sans nom sont indiscernables. - Critère 7.3 : contrôle au clavier et au pointeur. Les flèches doivent déplacer le focus entre les contrôles, et chaque contrôle rester activable à la souris.
- Critère 10.7 : prise de focus visible. Le focus réel se déplace de contrôle en contrôle et doit rester visible à chaque déplacement.
- Critère 12.8 : ordre de tabulation cohérent. Le roving tabindex fait de la barre un arrêt de tabulation unique et cohérent dans l'ordre de la page.
- Critère 12.9 : absence de piège au clavier. Tab doit toujours quitter la barre d'outils ; un
preventDefaultglobal sur le clavier la transforme en piège. - Critère 3.1 : information donnée par la couleur. L'état enfoncé d'un bouton à bascule (
aria-pressed) ne doit pas reposer sur la seule couleur : un liseré ou un fond nettement distinct doivent porter la même information.
Interaction clavier attendue
| Touche | Action |
|---|---|
| Tab | Entre dans la barre d'outils, sur le premier contrôle ou sur le dernier contrôle utilisé ; un nouvel appui sort de la barre entière |
| Maj + Tab | Sort de la barre d'outils |
| Flèche droite / Flèche gauche | Contrôle suivant / précédent (barre horizontale), avec bouclage optionnel |
| Flèche bas / Flèche haut | Contrôle suivant / précédent quand la barre est verticale (aria-orientation="vertical") |
| Début / Fin | (Optionnel) premier / dernier contrôle |
| Entrée / Espace | Activent le contrôle focalisé (comportement natif des boutons) |
Rôles et attributs ARIA
Le focus réel se déplace (appel à .focus()), jamais aria-activedescendant. aria-label nomme la barre ; aria-orientation="vertical" si elle est verticale, pour que les flèches attendues soient haut et bas.
<!-- Nommée, car une page peut en contenir plusieurs. Le roving tabindex
fait de toute la barre un arrêt de tabulation unique. -->
<div role="toolbar" aria-label="Mise en forme du texte" id="barre-mise-en-forme">
<!-- Exactement un tabindex="0" : le point d'entrée. -->
<button type="button" tabindex="0" aria-pressed="false">
<svg aria-hidden="true" focusable="false"><use href="#icone-gras"/></svg>
<span class="sr-only">Gras</span>
</button>
<button type="button" tabindex="-1" aria-pressed="false">
<svg aria-hidden="true" focusable="false"><use href="#icone-italique"/></svg>
<span class="sr-only">Italique</span>
</button>
<!-- Séparateur décoratif : role="separator" SANS tabindex, ce n'est pas
le widget de redimensionnement, juste un trait. -->
<span role="separator" aria-orientation="vertical"></span>
<button type="button" tabindex="-1">
<svg aria-hidden="true" focusable="false"><use href="#icone-lien"/></svg>
<span class="sr-only">Insérer un lien</span>
</button>
</div>
const barre = document.getElementById('barre-mise-en-forme');
const controles = [...barre.querySelectorAll('button')];
function focaliser(indice) {
// tabindex et focus bougent ensemble : l'unique arrêt de tabulation de la
// barre doit toujours être le dernier contrôle utilisé, pour qu'un retour
// par Tab retombe au même endroit.
controles.forEach((controle, i) =>
controle.setAttribute('tabindex', i === indice ? '0' : '-1'));
controles[indice].focus();
}
barre.addEventListener('keydown', (evenement) => {
const indice = controles.indexOf(document.activeElement);
if (indice === -1) return;
let suivant = null;
if (evenement.key === 'ArrowRight') suivant = (indice + 1) % controles.length;
if (evenement.key === 'ArrowLeft') suivant = (indice - 1 + controles.length) % controles.length;
if (evenement.key === 'Home') suivant = 0;
if (evenement.key === 'End') suivant = controles.length - 1;
// Tab, Entrée et Espace passent sans interception : preventDefault
// uniquement pour les touches réellement gérées.
if (suivant === null) return;
evenement.preventDefault();
focaliser(suivant);
});
Les boutons à bascule portent aria-pressed, et l'état enfoncé doit rester perceptible autrement que par la seule couleur (critère 3.1) : un liseré, un fond nettement distinct, ou les deux.
/* Le focus réel se déplace ici : :focus-visible fonctionne. */
[role="toolbar"] button:focus-visible {
outline: 2px solid #0056b3;
outline-offset: -2px;
}
/* État enfoncé : pas la couleur seule. */
[role="toolbar"] button[aria-pressed="true"] {
background: #0056b3;
color: #fff;
box-shadow: inset 0 0 0 2px #003d80;
}
Défauts fréquents et impact utilisateur
role="toolbar"sans roving tabindex : LE bug du motif. Le rôle annonce « barre d'outils » et promet un arrêt de tabulation unique, mais chaque bouton reste un arrêt distinct. La promesse est rompue ; c'est pire que l'absence de rôle.- Plusieurs contrôles en
tabindex="0": le modèle roving est rompu, les contrôles redeviennent des arrêts de tabulation séparés. - Barre d'outils sans nom accessible : deux barres annoncées « barre d'outils », sans qu'on sache laquelle met en forme et laquelle insère.
aria-orientation="vertical"sur une barre visuellement horizontale : l'utilisateur essaie haut et bas pendant que le script écoute gauche et droite ; rien ne bouge.preventDefaultappliqué à toutes les touches : Tab (la seule sortie), Entrée et Espace sont neutralisés ; la barre devient un piège au clavier.- Réinitialiser le focus au premier contrôle à chaque sortie : l'utilisateur qui revient par Tab perd sa position à chaque aller-retour, et recommence sa navigation aux flèches depuis le début.
Ce que les outils automatiques ne détectent pas
Un scanner automatique, le nôtre compris, repère une barre d'outils sans nom accessible. Mais le défaut central du motif lui échappe entièrement :
- le roving tabindex manquant :
role="toolbar"avec vingt arrêts de tabulation est de l'ARIA parfaitement valide, et parfaitement inutile. Seul un parcours clavier le révèle ; - le retour de position : revenir dans la barre doit ramener sur le dernier contrôle utilisé, pas sur le premier ; c'est un comportement, pas un attribut ;
- la cohérence entre l'orientation annoncée et les touches réellement gérées ;
- la pertinence même du rôle : trois boutons n'ont pas besoin d'une barre d'outils, et ce jugement reste humain.
Vérifier ce composant
Protocole manuel, trois minutes :
- Le test de l'arrêt unique, la raison d'être du motif : entrez dans la barre par Tab, puis appuyez à nouveau sur Tab. Vous devez vous retrouver après la barre entière. Si vous atterrissez sur le deuxième bouton, le roving tabindex manque et le rôle ment.
- Le test du retour : allez au troisième contrôle aux flèches, sortez par Tab, revenez par Maj + Tab. Vous devez retomber sur le troisième contrôle, pas sur le premier.
- Au lecteur d'écran : attendez-vous à « Mise en forme du texte, barre d'outils » puis « Gras, bouton à bascule, non enfoncé ». Si aucun nom de barre n'est annoncé, ajoutez
aria-label.
Deux outils gratuits du site pour appuyer la vérification :
- le testeur de focus visible valide vos styles
:focus: indispensable ici, puisque le focus réel se déplace à chaque flèche ; - le simulateur de lecteur d'écran affiche l'arbre d'accessibilité : le rôle
toolbar, son nom et les étatsaria-pressedde chaque bouton s'y lisent directement.
Approfondir avec les fiches critères
Vérifier ce composant sur votre site ?
Le scan repère les défauts détectables automatiquement (rôles incohérents, champs sans étiquette, attributs ARIA orphelins) ; le reste se vérifie à la main avec les protocoles de cette fiche.
Lancer un scan gratuit