Accessibilité Web : Piégeage du Focus, Régions ARIA Live et WCAG AA
Concevoir des modales, listes déroulantes et navigations clavier accessibles en React moderne.
1. Gestion du piégeage de focus sur les fenêtres modales
Une fenêtre modale qui ne piège pas le focus constitue un piège clavier dans le pire sens possible : un utilisateur de lecteur d’écran ou naviguant au clavier peut sortir de la modale visible avec Tab et atterrir dans le contenu de la page sous-jacente, invisible mais toujours interactif, perdant toute notion de l’endroit où il se trouve. Un piégeage de focus correct signifie que Tab et Maj+Tab ne parcourent que les éléments focalisables à l’intérieur de la boîte de dialogue, que le premier élément focalisable (ou le conteneur de dialogue lui-même) reçoit le focus à l’ouverture, et qu’Échap ferme la boîte de dialogue et rend le focus à l’élément qui l’a déclenchée — pas au sommet de la page, ce qui désoriente les utilisateurs naviguant de façon linéaire.
L’implémentation naïve — interroger manuellement tous les éléments focalisables et entourer Tab de gestionnaires de touches — est trompeusement difficile à réaliser correctement : il faut gérer le contenu ajouté ou retiré dynamiquement à l’intérieur de la boîte de dialogue, les éléments `disabled` et `tabindex="-1"` à ignorer, ainsi que les iframes, qui n’exposent pas leurs éléments focalisables internes à un simple `querySelectorAll`. C’est exactement le type de problème qu’il vaut mieux résoudre avec une bibliothèque maintenue (`focus-trap`, le `Dialog` de Radix UI, `useDialog` + `FocusScope` de React Aria) plutôt que de le réimplémenter, car les cas limites comptent vraiment pour les utilisateurs réels et sont faciles à manquer en test manuel.
Le rôle et le libellé comptent autant que la mécanique de focus. Le conteneur de la boîte de dialogue a besoin de `role="dialog"` (ou `alertdialog` pour les confirmations interruptives) et de `aria-modal="true"` pour que les technologies d’assistance sachent que le contenu extérieur est inerte, plus un nom accessible via `aria-labelledby` pointant vers le titre visible de la boîte de dialogue — une modale avec un piège de focus mais sans nom s’annonce comme une région sans étiquette, ce qui est presque aussi désorientant que l’absence totale de piège.
2. Annonces dynamiques avec ARIA Live
Les applications React modernes mettent à jour le DOM en permanence sans navigation de page, et les lecteurs d’écran n’ont aucun moyen de savoir qu’une erreur de validation de formulaire, une notification toast ou une confirmation « enregistré » vient d’apparaître, sauf si cette région est explicitement marquée comme « live ». Une région `aria-live` indique à la technologie d’assistance de surveiller un sous-arbre du DOM et d’annoncer les changements qui s’y produisent au fur et à mesure, sans que l’utilisateur ait besoin de naviguer manuellement jusqu’à cette région.
`aria-live="polite"` convient à l’écrasante majorité des cas — mises à jour de statut, confirmations d’enregistrement, compteurs de résultats de recherche — car le lecteur d’écran attend d’avoir terminé son annonce en cours avant d’énoncer la mise à jour, sans jamais interrompre l’utilisateur en pleine phrase. `aria-live="assertive"` doit être réservé aux interruptions réellement urgentes — un avertissement d’expiration de session, un paiement échoué — car il interrompt ce que le lecteur d’écran est en train de dire, ce qui est désagréable et doit rester rare, plutôt que d’être le réglage par défaut pour tout ce qui paraît « important » à un développeur voyant.
Un détail d’implémentation subtil mais critique : l’élément de région live doit exister dans le DOM avant que le changement de contenu ne se produise, sinon de nombreux lecteurs d’écran ne détectent pas l’annonce du tout. Cela signifie qu’il faut généralement un conteneur de région live persistant, monté une seule fois et vide au départ, dont le contenu textuel est mis à jour via l’état, plutôt que de rendre conditionnellement l’élément de région live entier en même temps que son contenu — ce dernier pattern échoue fréquemment en silence car l’annonce et la création de la région ont lieu dans le même rendu.
3. Construire des comboboxes et une navigation clavier accessibles
Une combobox personnalisée (recherche avec autocomplétion, saisie de tags, sélecteur avec recherche) est l’un des composants les plus risqués à construire de zéro, car elle combine un champ texte, une liste d’options et une interaction clavier complexe en un seul widget, et les pratiques ARIA à respecter (`role="combobox"` avec `aria-expanded`, `aria-controls`, et `aria-activedescendant` pointant vers l’option actuellement surlignée sans déplacer réellement le focus DOM hors du champ) sont faciles à mal implémenter de façon subtile, en passant une vérification visuelle rapide mais en échouant complètement pour les utilisateurs de lecteurs d’écran.
Le contrat clavier attendu par les utilisateurs est précis et non négociable : les flèches Bas/Haut déplacent l’option active sans retirer le focus du champ texte, Entrée sélectionne l’option active, Échap ferme la liste sans effacer la valeur actuelle du champ, et Début/Fin sautent à la première/dernière option. S’écarter de ce contrat — par exemple, déplacer réellement le focus vers chaque élément de liste au fil des flèches — casse l’annonce du rôle du champ par le lecteur d’écran et déroute les utilisateurs habitués au pattern combobox standard de toutes les autres applications qu’ils utilisent.
Vu la complexité, c’est l’un des cas les plus nets où coder soi-même perd face à une bibliothèque testée. Les primitives `Combobox` de Radix UI (via `cmdk` ou équivalent), `useComboBox` de React Aria, ou le `Combobox` de Headless UI implémentent tous correctement le pattern complet des pratiques d’auteur WAI-ARIA, y compris le câblage de `aria-activedescendant` et les subtilités de timing précises sur le moment où la liste doit être annoncée comme contenant des options ou comme vide.
4. Contrastes de couleurs et cibles tactiles
Le niveau WCAG AA exige un ratio de contraste d’au moins 4,5:1 pour le texte normal et 3:1 pour le texte large (18pt+, ou 14pt+ en gras) par rapport à son arrière-plan — c’est une vérification mathématique sur la luminance relative, pas une appréciation subjective, et des outils comme l’inspecteur d’accessibilité intégré au navigateur ou le WebAIM Contrast Checker la calculent directement à partir des valeurs hexadécimales. La violation la plus fréquente dans les design systems modernes n’est pas le texte principal — ce sont les éléments d’UI secondaires : le texte de placeholder, les boutons qui paraissent désactivés mais sont en réalité cliquables, et les icônes-boutons à faible opacité qui semblent intentionnellement discrètes pour un designer voyant mais échouent au test de contraste pour les utilisateurs malvoyants.
La taille des cibles tactiles est une exigence complémentaire souvent négligée : le critère WCAG 2.5.8 (niveau AA, ajouté avec WCAG 2.2) exige des cibles interactives d’au moins 24x24 pixels CSS, et les guides de plateforme d’Apple et de Google recommandent respectivement 44x44 et 48x48 pour une précision tactile confortable. Une rangée de petits boutons-icônes sans espacement entre eux est un échec courant — visuellement distincts sur une maquette de design, mais réellement difficiles à toucher précisément pour des utilisateurs avec des troubles moteurs ou simplement quiconque utilise l’application d’une seule main sur téléphone.
Aucune de ces deux vérifications n’exige un audit manuel à grande échelle — le contraste peut être vérifié automatiquement en CI avec des outils comme `axe-core` intégré à vos tests de composants, et la taille des cibles peut être imposée comme contrainte de design token (hauteur minimale de bouton/icône-bouton) plutôt que confiée à une revue au cas par cas. Détecter ces problèmes en CI est nettement moins coûteux que les détecter lors d’un audit d’accessibilité après lancement.
5. Synthèse d’accessibilité
Les échecs d’accessibilité dans les UI modernes à base de composants se regroupent systématiquement autour des trois mêmes mécanismes : la gestion du focus (où l’utilisateur peut-il aller, et cela correspond-il à ce qu’il voit), la communication sémantique (la technologie d’assistance sait-elle ce qui a changé et à quoi sert un contrôle), et l’affordance physique/visuelle (le contrôle peut-il réellement être vu et touché). Corriger un composant commence par se demander lequel de ces trois axes il échoue, plutôt que de traiter l’accessibilité comme une checklist indifférenciée.
La voie pragmatique consiste à construire les primitives interactives (dialogues, comboboxes, menus, infobulles) par-dessus une bibliothèque de composants accessibles maintenue, plutôt que de réimplémenter les patterns ARIA de zéro pour chaque projet — les pratiques d’auteur sont réellement complexes, les bibliothèques bien documentées ont déjà absorbé des années de corrections issues de tests réels avec lecteurs d’écran, et votre temps d’ingénierie est mieux investi sur la logique métier réelle de votre produit.
Intégrez des vérifications automatisées (axe-core en CI, lint de contraste sur les design tokens, contraintes de taille de cible dans votre bibliothèque de composants) dans le pipeline pour détecter les régressions avant la fusion, et complétez l’automatisation par des tests manuels périodiques avec un vrai lecteur d’écran (VoiceOver, NVDA) et une navigation exclusivement au clavier — les outils automatisés détectent de façon fiable environ un tiers des échecs WCAG ; le reste exige qu’un humain navigue réellement l’interface comme le ferait un utilisateur concerné.

