English

ARIA, ce scalpel de l'accessibilité web

Table des matières

Un outil de précision

Le rapport WebAIM Million 2026 est sans appel : les pages qui utilisent ARIA ont plus d'erreurs d'accessibilité que celles qui n'en utilisent pas. J'explique pourquoi dans ARIA, ce poison de l'accessibilité web, avec les quatre règles que le W3C a posées pour limiter les dégâts.

Mais ARIA n'est pas un poison par nature. C'est un scalpel : indispensable entre les bonnes mains, dangereux entre les mauvaises. Le HTML a des limites réelles, et quand tu les atteins, ARIA est la seule réponse.

Cet article va te montrer dans quels cas ARIA est le bon outil, quels anti-patterns éviter à tout prix, et comment tester en profondeur avec un lecteur d'écran.

Quand ARIA est la bonne réponse

Si la première règle du W3C est "n'utilise pas ARIA", c'est qu'ARIA existe pour les cas où le HTML ne suffit pas. Et ces cas existent. Pas sur chaque page, mais quand tu les rencontres, ARIA est la seule réponse.

Le contenu qui change sans prévenir

Un message de succès qui apparaît après l'envoi d'un formulaire. Un compteur de panier qui se met à jour. Une notification d'erreur de connexion. Un message dans un chat. Dans tous ces cas, le contenu change visuellement, mais le lecteur d'écran ne le sait pas. Il lit la page telle qu'elle était au chargement. Sans instruction contraire, le nouveau contenu est invisible.

C'est le problème que résolvent les live regions, le seul mécanisme qui permet de dire au lecteur d'écran "quelque chose a changé, annonce-le". Sara Soueidan en détaille chaque nuance dans sa série de référence sur le sujet.

live-regions.html
<!-- Confirmation non urgente — le lecteur d'écran
     attend une pause dans sa lecture -->
<p aria-live="polite">
  Article ajouté au panier (3 articles).
</p>
 
<!-- Erreur critique — le lecteur d'écran
     interrompt immédiatement sa lecture -->
<p aria-live="assertive">
  Connexion perdue. Tes modifications ne sont pas
  enregistrées.
</p>

aria-live="polite" est une notification discrète : le lecteur d'écran attend que l'utilisateur soit disponible avant d'annoncer le changement. aria-live="assertive" est une interruption : elle coupe ce que le lecteur d'écran est en train de dire pour annoncer le message immédiatement. La règle est simple : si l'utilisateur peut attendre, c'est polite. Si l'information est critique et urgente, c'est assertive.

L'élément <output> porte un rôle implicite status avec une live region polite, mais il est conçu pour les résultats de calcul, pas pour les notifications génériques. Pour le cas général, des confirmations d'action aux alertes d'erreur, aucun élément HTML natif ne couvre le besoin. C'est un cas où ARIA est la seule réponse.

Les widgets que le HTML n'a pas

Le HTML a des boutons, des liens, des cases à cocher, des champs de texte, des listes déroulantes. Mais il n'a pas d'onglets, pas d'arborescence dépliable, pas de combobox riche avec filtrage et groupes.

Le GOV.UK Design System et Heydon Pickering dans Inclusive Components montrent comment construire des onglets accessibles à partir d'une liste de liens internes : une <ul> avec des <li> et des <a href="#section">. Sans JavaScript, c'est une table des matières fonctionnelle. Avec JavaScript, la liste devient un groupe d'onglets :

tabs-progressive.html
<!-- Sans JavaScript : une table des matières -->
<ul>
  <li><a href="#general">Général</a></li>
  <li><a href="#securite">Sécurité</a></li>
  <li><a href="#notifications">Notifications</a></li>
</ul>
 
<!-- Avec JavaScript : les rôles ARIA
     transforment la sémantique -->
<ul role="tablist">
  <li role="presentation">
    <a role="tab" href="#general" id="tab-general"
       aria-controls="general" aria-selected="true">Général</a>
  </li>
  <li role="presentation">
    <a role="tab" href="#securite" id="tab-securite"
       aria-controls="securite" aria-selected="false"
       tabindex="-1">Sécurité</a>
  </li>
  <li role="presentation">
    <a role="tab" href="#notifications" id="tab-notifications"
       aria-controls="notifications" aria-selected="false"
       tabindex="-1">Notifications</a>
  </li>
</ul>
 
<section id="general" role="tabpanel" aria-labelledby="tab-general">
  <!-- ... -->
</section>

La <ul> devient un tablist, les <a> deviennent des tab, jusque-là, c'est la sémantique qui évolue avec le comportement. Mais les <li> posent un problème : dans un tablist, il n'y a pas de "liste d'éléments". Si tu les laisses tels quels, le lecteur d'écran annonce "liste, 3 éléments, onglet, Général", un mélange de deux sémantiques qui n'a aucun sens. Le role="presentation" efface la sémantique de list item sans retirer l'élément du DOM, c'est exactement le cas prévu par la deuxième règle d'ARIA : modifier la sémantique native quand elle entre en conflit avec le rôle réel de l'élément.

Le balisage ne fait que la moitié du chemin. Chaque <a> pointe vers son panneau via aria-controls, chaque panneau reçoit role="tabpanel" et aria-labelledby, et le JavaScript doit encore fournir la couche clavier : flèches gauche/droite pour circuler entre les onglets (le tabindex="-1" sort les onglets inactifs du parcours Tab, c'est le roving tabindex du pattern de l'APG), bascule d'aria-selected, masquage des panneaux inactifs. Sans cette couche, le composant annonce "onglet" mais se comporte comme une liste de liens : la promesse du rôle n'est pas tenue.

Le même principe s'applique aux tree views (une arborescence navigable au clavier) ou aux combobox riches (un champ avec filtrage, groupes et navigation clavier que <datalist> ne couvre pas). Dans tous ces cas, ARIA ne remplace pas le HTML, il le complète.

Les états que le HTML ne peut pas exprimer

Un menu déroulant est ouvert ou fermé. Un lien pointe vers la page courante. Un champ est lié à un message d'aide. Le HTML ne sait pas exprimer ces états, ARIA le fait :

aria-states.html
<!-- Le bouton annonce "Menu, réduit" ou
     "Menu, développé" -->
<button aria-expanded="false"
        aria-controls="menu-navigation">
  Menu
</button>
<ul id="menu-navigation" hidden>
  <li><a href="/produits">Produits</a></li>
  <li><a href="/contact">Contact</a></li>
</ul>
 
<!-- Le lecteur d'écran annonce
     "Accueil, lien, page actuelle" -->
<nav aria-label="Navigation principale">
  <a href="/" aria-current="page">Accueil</a>
  <a href="/produits">Produits</a>
  <a href="/contact">Contact</a>
</nav>
 
<!-- Le message d'aide est lu après le champ -->
<label for="email">Adresse email</label>
<input id="email" type="email"
       aria-describedby="email-aide">
<p id="email-aide">
  Utilisée uniquement pour la confirmation
  de commande.
</p>

aria-expanded transforme un bouton muet en bouton qui annonce son état. aria-current="page" dit à l'utilisateur où il se trouve dans la navigation, sans ça, tous les liens se ressemblent. aria-describedby associe un texte d'aide à un champ sans toucher à son label : le lecteur d'écran lit d'abord le label, puis la description complémentaire.

Aucun attribut HTML natif ne fait ce travail. C'est ARIA dans son rôle exact : exprimer ce que le HTML ne peut pas dire.

Les anti-patterns qui empoisonnent

Tu sais maintenant quand ARIA est la bonne réponse. Léonie Watson navigue le web au lecteur d'écran huit heures par jour, et ce qu'elle entend le plus souvent, ce sont les mêmes erreurs qui reviennent. Voici les plus répandues.

Le nom qui contredit l'étiquette

label-mismatch.html
<!-- ❌ Le bouton affiche "Rechercher" mais
     le lecteur d'écran annonce "Trouver un produit" -->
<button aria-label="Trouver un produit">
  Rechercher
</button>

L'utilisateur qui navigue à la voix voit le mot "Rechercher" et dit "clic Rechercher". Rien ne se passe, parce que le nom accessible du bouton est "Trouver un produit", pas "Rechercher". Le aria-label a remplacé le texte visible dans l'arbre d'accessibilité.

Le RGAA l'interdit à travers trois critères : 11.2 pour les champs de formulaire, 11.9 pour les boutons, 6.1 pour les liens : l'intitulé visible doit être contenu dans le nom accessible. Si tu veux ajouter du contexte, utilise aria-describedby, il complète le nom sans le remplacer :

label-match.html
<!-- ✅ Le texte visible est le nom accessible,
     le contexte supplémentaire est en description -->
<button aria-describedby="search-help">
  Rechercher
</button>
<p id="search-help" hidden>
  Dans le catalogue produits
</p>

Le contenu que tu fais disparaître

aria-hidden-danger.html
<!-- ❌ Le produit épuisé est grisé visuellement,
     le dev le masque aussi pour le lecteur
     d'écran — mais le lien reste focusable -->
<article class="product sold-out"
         aria-hidden="true">
  <h3>Casque audio sans fil</h3>
  <p>89,99 € — <strong>Épuisé</strong></p>
  <a href="/product/wireless-headphones">Voir le produit</a>
</article>

Le développeur a grisé le produit épuisé et pensait logiquement le masquer aussi pour le lecteur d'écran. Mais l'information reste utile : le prix, la disponibilité, le lien vers la fiche produit. Et surtout, aria-hidden="true" est hérité par tous les descendants : le lien disparaît de l'arbre d'accessibilité mais reste focusable au clavier. L'utilisateur arrive sur un élément muet, pas de nom, pas de destination.

Visuellement atténué ne veut pas dire inutile. Si le contenu doit rester dans la page, il doit rester dans l'arbre d'accessibilité :

aria-hidden-fix.html
<!-- ✅ Le produit reste accessible,
     son état est annoncé clairement -->
<article class="product sold-out">
  <h3>Casque audio sans fil</h3>
  <p>89,99 € — <strong>Épuisé</strong></p>
  <a href="/product/wireless-headphones">Voir le produit</a>
</article>
 
<!-- ✅ Si le contenu doit vraiment disparaître,
     hidden le retire du rendu ET de l'arbre
     d'accessibilité -->
<article class="product sold-out" hidden>
  <h3>Casque audio sans fil</h3>
  <p>89,99 € — <strong>Épuisé</strong></p>
  <a href="/product/wireless-headphones">Voir le produit</a>
</article>

Les rôles qui ne servent à rien

redundant-roles.html
<!-- ❌ Redondant — ces éléments HTML portent
     déjà ces rôles implicitement -->
<nav role="navigation">...</nav>
<main role="main">...</main>
<header role="banner">...</header>
<footer role="contentinfo">...</footer>
<button role="button">Envoyer</button>
<a role="link" href="/contact">Contact</a>

Un <nav> est un navigation. Un <main> est un main. Un <button> est un button. Ajouter le rôle explicite sur un élément qui le porte déjà, c'est écrire color: red; color: red; en CSS : ça ne casse rien, mais ça signale que le développeur ne sait pas ce que l'élément fait nativement. Et ce bruit masque les vrais problèmes dans un audit.

Le aria-label que personne n'entend

aria-label-static.html
<!-- ❌ aria-label sur un paragraphe —
     ignoré par la plupart des lecteurs d'écran -->
<p aria-label="Avertissement important">
  Ce produit contient des allergènes.
</p>

Le aria-label n'est pas un attribut universel. Ben Myers l'a documenté en détail : la spécification ARIA ne l'autorise que sur les éléments dont le rôle admet un nom (widgets, landmarks, images), pas sur les éléments statiques comme <p>, <span> ou <div> sans rôle. Un rôle explicite ne suffit d'ailleurs pas toujours : certains rôles comme presentation ou generic interdisent aussi le nommage ("Name from: prohibited" dans la spec). En pratique, la plupart des lecteurs d'écran l'ignorent sur ces éléments. Le texte du paragraphe est lu normalement, le aria-label est perdu.

Si le texte visible ne suffit pas, la solution n'est pas un aria-label invisible, c'est un meilleur texte visible. J'en montre un exemple concret dans Autopsie d'une card produit, où chaque aria-label est pesé au cas par cas :

better-text.html
<!-- ✅ Le texte dit ce qu'il faut dire,
     pour tout le monde -->
<p><strong>Avertissement :</strong>
  ce produit contient des allergènes.</p>

Le bouton sans nom

accessible-name.html
<!-- ❌ Pas de nom accessible — le lecteur d'écran
     annonce juste "bouton" -->
<button>
  <svg viewBox="0 0 24 24"><!-- icon --></svg>
</button>
 
<!-- ✅ aria-label quand l'icône seule
     suffit visuellement -->
<button aria-label="Fermer la modale">
  <svg viewBox="0 0 24 24" aria-hidden="true">
    <!-- icon -->
  </svg>
</button>
 
<!-- ✅✅ Encore mieux : du texte visible
      à côté de l'icône -->
<button>
  <svg viewBox="0 0 24 24" aria-hidden="true">
    <!-- icon -->
  </svg>
  Fermer
</button>

Un bouton avec juste une icône SVG, sans texte ni aria-label, est annoncé "bouton", sans aucune indication de ce qu'il fait. Le aria-label corrige le problème pour les lecteurs d'écran, mais pas pour les personnes qui naviguent à la voix : elles voient une icône et ne savent pas quel mot prononcer pour activer le bouton. Le texte visible est la solution la plus robuste : il sert à la fois de nom accessible, de repère pour la navigation vocale, et de secours quand le CSS ne charge pas.

Tester en profondeur

Les trois vérifications en 30 secondes de l'article précédent utilisent l'arbre d'accessibilité des outils de développement. C'est un bon premier filtre. Mais pour tester ce que l'utilisateur entend réellement, il faut activer un lecteur d'écran.

VoiceOver sur macOS

Active VoiceOver avec Cmd + F5 (Fn + Cmd + F5 sur les claviers récents, ou triple pression sur Touch ID). Navigue ta page avec Tab (éléments interactifs) et Ctrl + Option + flèches (lecture séquentielle). Écoute ce que le lecteur d'écran annonce pour chaque élément : le rôle, le nom, l'état.

Le rotor (Ctrl + Option + U) est ton outil d'audit structurel. Il affiche la liste des titres, des liens, des landmarks et des contrôles de formulaire. Si un élément manque dans le rotor alors qu'il devrait y être, ou s'il y apparaît avec un nom incompréhensible, ton ARIA a un problème.

Les autres lecteurs d'écran

VoiceOver n'est pas le seul outil, et tes utilisateurs n'utilisent pas tous macOS :

  • NVDA + Firefox sur Windows : gratuit et open source, le plus utilisé par les développeurs sous Windows. Navigation par Tab (éléments interactifs) et flèches (lecture séquentielle). La liste d'éléments (Insert + F7) joue le même rôle que le rotor.
  • JAWS + Chrome ou Edge sur Windows : la référence professionnelle, payant. Même logique de navigation, avec la liste de liens (Insert + F7) et la liste de titres (Insert + F6).
  • TalkBack + Chrome sur Android : le lecteur d'écran mobile. Navigation par balayage (gauche/droite pour le séquentiel, haut/bas pour changer de mode de navigation). Le menu de lecture (balayage vers le bas puis vers la droite) donne accès aux titres, liens et contrôles.

Chaque lecteur d'écran a ses particularités, mais les signaux d'alerte restent les mêmes.

Ce qu'il faut écouter

Quatre signaux d'alerte :

  1. Un rôle annoncé sans comportement. Le lecteur d'écran dit "bouton" mais Entrée ne fait rien. Tu as un role="button" sans gestionnaire clavier.

  2. Un nom qui ne correspond pas. Le lecteur d'écran annonce "Trouver un produit" mais tu lis "Rechercher" à l'écran. Tu as un aria-label qui contredit le texte visible.

  3. Un silence là où tu attends du contenu. Tu navigues avec Tab et le focus s'arrête sur un élément muet. Tu as un aria-hidden="true" sur un élément focusable.

  4. Une annonce qui ne vient pas. Tu soumets un formulaire, un message de succès apparaît, mais le lecteur d'écran ne dit rien. Il manque un aria-live sur la zone de notification.

Chacun de ces signaux correspond à un anti-pattern de la section précédente. Le lecteur d'écran ne fait que révéler ce que l'arbre d'accessibilité contenait déjà.

Les critères d'ensemble

Chaque cas que tu viens de voir, les live regions, les onglets, les états, les anti-patterns, le RGAA les traduit en critères vérifiables :

  • RGAA 6.1 : chaque lien est explicite
  • RGAA 7.1 : chaque script est compatible avec les technologies d'assistance (ses tests couvrent la conformité des rôles, états et propriétés ARIA)
  • RGAA 7.3 : chaque script est contrôlable au clavier et par tout dispositif de pointage
  • RGAA 11.2 : chaque étiquette de formulaire est pertinente
  • RGAA 11.9 : chaque intitulé de bouton est pertinent

Tous ces critères convergent vers la même idée : chaque rôle posé est un engagement, le composant doit se comporter comme ce rôle le promet. Chaque nom accessible est un contrat, il doit correspondre à ce que l'utilisateur voit et peut prononcer. L'arbre d'accessibilité et le lecteur d'écran te disent si tu le tiens.

Ouvre ta page, lance VoiceOver ou NVDA, et écoute. Trente secondes suffisent pour entendre ce que le lecteur d'écran annonce.

Tu as le scalpel. Il te reste à l'utiliser.

Sources