L'accessibilité
Un site utilisable au clavier et avec un lecteur d'écran : la langue de la page, des menus qui s'ouvrent autrement qu'au survol, un lien d'évitement, des noms pour ce qui n'a pas de texte, des animations qui s'effacent sur demande, et ce qui reste à faire.
Précédemment. Le header réunit la navigation, la connexion et les signaux de la personne connectée, en deux rendus : une barre et un panneau mobile.
Tout le monde ne navigue pas à la souris. Une personne malvoyante utilise un lecteur d'écran, qui lit la page à voix haute et se pilote au clavier. D'autres n'utilisent que le clavier, par choix ou parce qu'un handicap moteur rend la souris difficile. D'autres encore demandent à leur système de réduire les animations, qui leur donnent des vertiges. Une page accessible fonctionne pour chacun. Les règles de référence sont les WCAG (internationales) et leur déclinaison française, le RGAA. Ce chapitre décrit ce que le site applique, une correction récente du header, et ce qui reste à faire.
Quatre questions pour chaque élément#
Rendre un site accessible revient à se poser quatre questions, élément par élément :
Question | Exemple dans le projet |
|---|---|
Peut-on l’atteindre et l’utiliser au clavier ? | Les menus du header, le panneau mobile (Échap pour fermer). |
A-t-il un nom, pour qui ne le voit pas ? | Le bouton burger « Ouvrir le menu », les flèches du carrousel « Précédent » et « Suivant ». |
Annonce-t-il son état ? | aria-expanded sur les boutons qui déroulent un menu, aria-current="page" sur le lien de la page en cours — premier niveau compris, qui l’oubliait. |
Bouge-t-il sans qu’on l’ait demandé ? | Les cloches et le ruban des flashs info, arrêtés si le système demande moins d’animations. |
Showing entries1to4entries
La première information donnée à un lecteur d'écran est la langue de la page : elle choisit la voix et la prononciation. Le layout racine la déclare une fois pour tout le site (<html lang="fr">).
Au clavier : des menus qui ne s'ouvrent pas qu'au survol#
Au clavier, la touche Tab passe d'un élément interactif à l'autre : liens, boutons, champs. Un simple texte n'en fait pas partie, même s'il réagit au survol. Jusqu'à récemment, les titres « Nos disciplines » et « Qui sommes-nous ? » de la barre étaient des textes ouverts au survol, comme le menu du membre (avatar et prénom). Au clavier, la touche Tab les sautait : une personne sans souris ne pouvait atteindre ni les pages de ces menus, ni son profil, ni même la déconnexion.
La correction suit le modèle de menu de navigation à volets (« disclosure ») des pratiques WAI-ARIA : le titre devient un vrai bouton, qui ouvre et ferme son menu. La logique est écrite une fois, dans un hook partagé par les trois menus :
useDisclosure — un menu à volets, souris et clavier
Clique une étape pour mettre en évidence la zone correspondante dans le fichier complet.
// ⚠ Fichier introuvable : apps/web/src/features/app-shell/useDisclosure.ts// Il a été déplacé ou supprimé depuis la rédaction de ce chapitre.Geste | Effet |
|---|---|
Tab jusqu’au titre, puis Entrée ou Espace | Ouvre ou ferme le menu ; le bouton annonce son état (aria-expanded). |
Tab dans le menu ouvert | Parcourt ses liens ; les liens d’un menu fermé sont masqués, donc sautés. |
Échap | Ferme le menu et remet le curseur sur son titre, pour ne pas perdre sa place. |
Tab après le dernier lien | Ferme le menu en sortant. |
Survol, clic ailleurs | Comme avant pour la souris : ouvre au survol, ferme en sortant ou en cliquant ailleurs. |
Showing entries1to5entries
Un détail évite un conflit entre souris et clavier. À la souris, le survol a déjà ouvert le menu : un clic sur le titre qui l'inverserait le refermerait sous le pointeur. Le bouton ne bascule donc que pour un « clic » produit par le clavier, que le navigateur signale par detail === 0 :
Le dernier pas du parcours ci-dessus évite un conflit entre souris et clavier.
Le curseur clavier doit aussi rester visible. Les nouveaux boutons portent un contour émeraude quand ils le reçoivent au clavier (focus-visible:outline-emerald-400), et pas au clic de souris, où il serait inutile.
Les infobulles qui contiennent des liens#
Les cloches et l'entrée « Mes documents » montrent au survol une infobulle détaillée, avec des liens. Une classe Tailwind de plus les affiche aussi quand l'élément reçoit le curseur clavier :
group-focus-within s'applique quand un élément du groupe, la cloche puis chacun des liens, a le curseur : l'infobulle reste ouverte pendant qu'on parcourt ses liens.
Le lien d'évitement#
Au clavier, chaque page commence par le header : logo, sept entrées de menu, thème, cloches, compte. Les traverser à chaque page pour atteindre le contenu est épuisant. Le premier arrêt de la touche Tab est donc un lien « Aller au contenu », invisible jusqu'à ce qu'il reçoive le curseur (sr-only focus:not-sr-only). Il pose le curseur sur l'élément <main> de la page :
Le saut d'ancre classique (href="#contenu") ferait défiler la page jusqu'au contenu, dont le haut glisserait sous le header collant. Le lien pose donc le curseur sans défiler (preventScroll) ; l'ancre reste en place pour un navigateur sans JavaScript. <main> porte tabIndex={-1} : il peut recevoir le curseur sans devenir lui-même un arrêt de la touche Tab, et le Tab suivant part du contenu.
Un nom pour ce qui n'a pas de texte#
Un bouton fait d'une seule icône n'a rien à lire pour un lecteur d'écran, qui annoncerait seulement « bouton ». Il reçoit un nom par aria-label : « Ouvrir le menu » pour le burger, « Mettre en pause » ou « Reprendre le défilement » pour le carrousel. Les cloches vont plus loin : leur nom est le message de leur infobulle (« Vous avez 3 contenus en attente et 2 dans la corbeille »).
À l'inverse, une image purement décorative, comme le chevron d'un menu, est cachée aux lecteurs d'écran (alt="" et aria-hidden="true") : l'annoncer n'apporterait que du bruit. Une image qui porte une information, comme le logo, a un texte de remplacement (alt).
Des animations qui s'effacent sur demande#
Les systèmes d'exploitation proposent un réglage « réduire les animations ». Le navigateur le transmet par la requête média prefers-reduced-motion. Le ruban des flashs info sous le header défile en continu ; avec ce réglage, il devient une liste fixe qu'on fait défiler soi-même :
Le ruban s'arrête aussi au survol et quand un de ses titres reçoit le curseur clavier (:focus-within) : on ne lit pas un bouton qui file. Les cloches qui s'agitent et les transitions de la visionneuse de la galerie suivent la même règle.
Le carrousel de l'accueil défile automatiquement, avec un bouton de pause : tout contenu qui bouge plus de cinq secondes doit pouvoir être arrêté. Pour qui a demandé moins d'animations, il démarre arrêté. Le réglage est lu comme une source extérieure à React, avec useSyncExternalStore : false pendant le rendu serveur, qui ne le connaît pas, puis la vraie valeur dès l'hydratation, et le carrousel suit si le réglage change. Le choix du visiteur, une fois qu'il a appuyé sur Lecture ou Pause, l'emporte :
Ce qui reste à faire#
Le site n'a pas encore fait l'objet d'un audit complet. Deux chantiers sont connus :
- Les contrastes. Les couleurs n'ont pas été mesurées, notamment en thème sombre. Le texte courant doit atteindre un rapport de contraste de 4,5 pour 1 avec son fond.
- Des tests automatiques. Un outil comme axe peut vérifier à chaque modification une partie de ces règles (noms manquants, contrastes, rôles). Il ne remplace pas un essai au clavier et avec un lecteur d'écran, mais il évite les régressions.
L'accessibilité ne s'ajoute pas à la fin : un élément interactif qui est un vrai <button> ou un vrai <a> est accessible presque gratuitement. Les difficultés viennent des <div> qui imitent un bouton, et des interactions réservées au survol.
Récapitulatif#
Question | Réponse du projet |
|---|---|
Comment ouvrir un menu du header sans souris ? | Son titre est un bouton : Tab, puis Entrée ou Espace ; Échap le ferme (useDisclosure). |
Comment un lecteur d’écran nomme-t-il un bouton-icône ? | Par son aria-label ; une image décorative est cachée (alt vide, aria-hidden). |
Comment un lecteur d’écran sait-il qu’un menu est ouvert ? | Par aria-expanded sur le bouton ; la page en cours est signalée par aria-current. |
Comment éviter de traverser le header à chaque page ? | Par le lien « Aller au contenu », premier arrêt de la touche Tab. |
Que deviennent les animations pour qui les a désactivées ? | prefers-reduced-motion les arrête : ruban en liste fixe, cloches immobiles, carrousel arrêté au départ. |
Qu’est-ce qui reste à faire ? | Mesurer les contrastes, ajouter des tests automatiques. |
Showing entries1to6entries
À suivre#
La partie « Interface transverse » est complète. Le projet doit maintenant prouver qu'il fonctionne, et continuer à fonctionner après chaque modification : vérifier les types, relire le code, déboguer, tester.
Partie suivante : qualité. La chaîne de pnpm check, le typecheck et le lint, et les gardes écrites pour ce projet.