Chapitre 10.1 — Le principe des patterns
⏱️ TL;DR — Un widget accessible n’est pas une œuvre d’art personnelle : c’est un contrat connu. L’ARIA Authoring Practices Guide (APG) du W3C décrit, pour chaque composant (modale, onglets, menu, combobox…), le rôle attendu, le nom accessible, les touches clavier à câbler, les états à tenir à jour et la gestion du focus. Suivre ces patterns, c’est offrir à l’utilisateur un comportement qu’il connaît déjà. À l’inverse, improviser un widget « original » le condamne à réapprendre — ou à ne pas pouvoir s’en servir. Et avant même d’ouvrir l’APG, pose-toi la vraie question : ai-je besoin d’un composant custom, ou le natif suffit-il ?
🎯 Objectifs
- Comprendre pourquoi on suit un pattern établi au lieu d’inventer.
- Connaître la check-list universelle d’un widget accessible.
- Distinguer les deux modèles de focus interne : roving tabindex et
aria-activedescendant. - Savoir quand ne pas coder un composant custom.
Pourquoi des patterns, et pas de l’improvisation
Quand un utilisateur de lecteur d’écran rencontre des onglets, il s’attend à un comportement précis : Tab pour entrer dans la barre d’onglets, les flèches pour changer d’onglet, l’annonce « onglet sélectionné, 2 sur 3 ». Ce comportement n’est pas une convention que tu décides : c’est un standard partagé, documenté dans l’APG, que les gens ont appris une fois pour toutes.
Improviser un widget « à ta sauce » revient à inventer une nouvelle langue et à espérer que l’utilisateur la parle. Il ne la parle pas. Le pattern APG n’est donc pas une contrainte bureaucratique : c’est le vocabulaire commun qui rend ton composant utilisable sans mode d’emploi.
💡 Réflexe — Avant d’écrire une seule ligne d’un widget custom, ouvre la page APG correspondante (ARIA Authoring Practices Guide,
w3.org/WAI/ARIA/apg). Elle te donne le rôle, le clavier attendu et les états à gérer, cas par cas. Tu ne devines pas : tu implémentes une spec. C’est le prolongement direct de la Partie 6 sur ARIA — l’APG, c’est ARIA mise en recettes.
La check-list universelle d’un widget
Tout composant interactif custom, quel qu’il soit, doit satisfaire cinq points. Retiens-les : ils reviennent à chaque chapitre de cette partie.
| # | Exigence | Question à te poser |
|---|---|---|
| 1 | Rôle correct | Le lecteur d’écran annonce-t-il ce que c’est ? (dialog, tab, menu…) |
| 2 | Nom accessible | Le widget a-t-il un libellé annoncé ? (aria-label, aria-labelledby) |
| 3 | Clavier attendu | Toutes les touches du pattern sont-elles câblées ? (flèches, Échap, Entrée…) |
| 4 | États à jour | aria-expanded, aria-selected, aria-checked suivent-ils l’état réel ? |
| 5 | Focus géré | Le focus va-t-il au bon endroit, et n’est-il jamais perdu ? |
ARIA (points 1, 2, 4) est la partie déclarative : elle informe l’arbre d’accessibilité. Mais les points 3 et 5 — clavier et focus — sont du JavaScript que personne n’écrit à ta place. C’est tout le sujet de cette partie : ARIA nomme le widget, toi tu le fais fonctionner.
<!-- ❌ Rôle déclaré, mais widget mort : aucune touche, aucun état géré -->
<div role="tablist">
<div role="tab">Profil</div>
<div role="tab">Sécurité</div>
</div>Ici l’ARIA ment : elle promet des onglets, mais les flèches ne font rien, rien n’est focusable, aucun aria-selected ne bouge. L’utilisateur entend « onglet » et… reste bloqué. Un role sans le comportement associé est un piège, pas une fonctionnalité.
Les deux modèles de focus interne
Dans un widget composite (onglets, menu, listbox), plusieurs éléments sont candidats au focus. Comment Tab et les flèches se répartissent-ils le travail ? L’APG propose deux modèles — tu en choisis un par widget, jamais les deux.
Modèle 1 — le roving tabindex. Un seul élément du groupe est focusable à la fois (tabindex="0"), tous les autres sont retirés de la tabulation (tabindex="-1"). Les flèches déplacent le tabindex="0" d’un élément à l’autre et appellent .focus(). Le focus réel du navigateur se promène (« rove ») dans le groupe.
<!-- Roving tabindex : un seul tab stop, les flèches déplacent le focus réel -->
<div role="tablist">
<button role="tab" tabindex="0" aria-selected="true">Profil</button>
<button role="tab" tabindex="-1" aria-selected="false">Sécurité</button>
</div>Modèle 2 — aria-activedescendant. Le focus réel ne bouge pas : il reste sur le conteneur (souvent un input). Un attribut aria-activedescendant pointe vers l’id de l’élément « virtuellement » actif, et c’est lui que le lecteur d’écran annonce. On l’utilise surtout pour les combobox, où le focus doit rester dans le champ de saisie pendant qu’on parcourt la liste.
<!-- aria-activedescendant : le focus reste dans l'input, l'option active est pointée -->
<input role="combobox" aria-expanded="true"
aria-controls="lb" aria-activedescendant="opt-2">
<ul role="listbox" id="lb">
<li role="option" id="opt-1">Paris</li>
<li role="option" id="opt-2" aria-selected="true">Lyon</li>
</ul>Règle simple : roving tabindex quand le focus peut réellement se déplacer sur les éléments (onglets, menu, radios) ; aria-activedescendant quand le focus doit rester ailleurs, typiquement dans un champ de saisie (combobox).
♿ Côté utilisateur — Ces deux modèles servent le même but : un seul « point d’arrêt » au
Tabpour tout le groupe. Sans ça, un utilisateur de menu à 30 entrées devrait tabuler 30 fois pour en sortir. Avec un roving tabindex, il entre auTab, choisit aux flèches, ressort auTabsuivant. Le widget se comporte comme un tout, pas comme 30 obstacles.
Quand NE PAS coder un composant custom
Le meilleur widget accessible est souvent celui que tu n’écris pas. Avant de te lancer, remonte cette échelle, du plus sûr au plus risqué :
- Un élément natif existe-t-il ?
<dialog>pour une modale,<details>pour une disclosure,<select>pour une liste simple. Le natif t’offre rôle, clavier et focus gratuitement. - Sinon, une lib accessible et éprouvée fait-elle le travail ? Radix UI, React Aria, Headless UI implémentent déjà les patterns APG, testés avec de vrais lecteurs d’écran (Partie 11).
- Sinon seulement, code le pattern APG toi-même — en sachant que tu signes pour le clavier, les états et le focus, à la main et pour de bon.
Recoder un combobox from scratch « parce que ça a l’air simple » est l’une des meilleures façons de produire un composant inutilisable. La compétence experte, ce n’est pas de tout réécrire : c’est de savoir quand ne pas le faire.
🧭 Sur A11yLearn — Sur A11yLearn, la première version du sélecteur de langue était une
<div role="listbox">maison, sans clavier ni gestion du focus. On a tranché avec l’échelle ci-dessus : un<select>natif couvrait exactement le besoin (choisir une valeur dans une courte liste). Trois lignes de HTML ont remplacé 200 lignes de JavaScript bancal — et le composant est devenu utilisable au clavier, au lecteur d’écran et en commande vocale d’un coup. Le custom n’est arrivé que là où le natif ne suffisait plus : l’autocomplétion de recherche (voir 10.4).
✏️ Exercices
Exercice 1 — Diagnostique le widget mort. Un collègue livre ce « menu » et jure qu’il est accessible parce qu’il a mis les rôles ARIA. Cite au moins trois choses qui manquent.
<div role="menu">
<div role="menuitem" onclick="aller('/profil')">Profil</div>
<div role="menuitem" onclick="aller('/reglages')">Réglages</div>
</div>✅ Solution
Il manque au moins : (1) le clavier — aucune touche n’est câblée, ni flèches pour naviguer, ni Échap pour fermer ; (2) le focus — les <div> ne sont pas focusables (pas de roving tabindex, aucun tabindex) ; (3) le nom accessible du menu et le bouton déclencheur avec aria-haspopup/aria-expanded qui manquent entièrement. Les rôles sont posés, mais les points 3 et 5 de la check-list (clavier, focus) sont absents : l’ARIA ment. Ici, une simple <nav> avec des <a> serait plus juste (voir 10.3).
Exercice 2 — Choisis le modèle de focus. Pour chacun, dis si tu utiliserais un roving tabindex ou aria-activedescendant : (a) une barre d’onglets, (b) un champ d’autocomplétion de ville, (c) un menu contextuel.
✅ Solution
(a) Roving tabindex : le focus peut réellement se poser sur chaque onglet. (b) aria-activedescendant : le focus doit rester dans l’input pendant que l’utilisateur tape et parcourt les suggestions. (c) Roving tabindex : le focus se déplace d’un menuitem à l’autre aux flèches. Le fil : aria-activedescendant dès que le focus réel doit rester dans un champ de saisie ; roving tabindex sinon.
🧠 Quiz de révision
1. Qu’est-ce que l’APG et à quoi sert-elle ?
L’ARIA Authoring Practices Guide du W3C : un catalogue de patterns décrivant, pour chaque widget, le rôle, le nom accessible, le clavier attendu, les états et la gestion du focus. Elle évite d’improviser : tu implémentes une spec connue de tous, donc un comportement que l’utilisateur connaît déjà.
2. Quels sont les cinq points de la check-list universelle ?
Rôle correct, nom accessible, clavier attendu, états à jour, focus géré. Les 1-2-4 sont déclaratifs (ARIA) ; les 3 et 5 (clavier, focus) sont du JavaScript que tu dois écrire toi-même.
3. Pourquoi un role sans comportement est-il un piège ?
role sans comportement est-il un piège ?Parce que l’ARIA promet un widget (« onglet », « menu ») que le clavier ne connaît pas : rien n’est focusable, aucune touche ne répond, aucun état ne bouge. Le lecteur d’écran annonce quelque chose d’inutilisable. Une ARIA fausse est pire que pas d’ARIA.
4. Quelle est la différence entre roving tabindex et aria-activedescendant ?
Avec le roving tabindex, le focus réel du navigateur se déplace d’un élément à l’autre (un seul a tabindex="0"). Avec aria-activedescendant, le focus réel ne bouge pas (il reste sur le conteneur, souvent un input) et un attribut pointe l’élément « actif » à annoncer. On réserve aria-activedescendant aux cas où le focus doit rester dans un champ de saisie (combobox).
5. Dans quel ordre envisager un widget interactif ?
D’abord l’élément natif (<dialog>, <details>, <select>), puis une lib accessible éprouvée (Radix, React Aria…), et en dernier recours seulement, coder le pattern APG à la main. Le meilleur widget accessible est souvent celui qu’on n’écrit pas.
Chapitre suivant : La modale (dialog) — le pattern le plus demandé, et le plus souvent raté : focus piégé, Échap, focus rendu au déclencheur.