Essentiel Mis à jour : 2026-04

Anti-patterns ARIA : Les erreurs fréquentes en audit

Les 10 erreurs ARIA les plus rencontrées en audit de sites WordPress : exemples concrets, ce que le lecteur d'écran entend réellement, et la correction dans chaque cas.

Table des matières

Un anti-pattern ARIA est une pratique qui semble correcte, car elle utilise les bons attributs, mais produit un résultat incorrect, voire pire qu’une absence d’ARIA. La plupart viennent d’une compréhension partielle de ce qu’ARIA communique réellement aux technologies d’assistance (AT).

Cet article documente les dix erreurs les plus fréquemment rencontrées en audit. Pour chacune : le code incorrect, ce que NVDA annonce réellement, et la correction.

Anti-pattern 1 : L’ARIA cargo cult

Description : copier-coller d’attributs ARIA vus sur un autre site ou dans une documentation, sans comprendre leur sémantique dans le contexte précis. L’ARIA est présent, mais incohérent avec le composant réel.

Exemple : aria-pressed sur un lien de navigation

<!-- ✗ aria-pressed n'a de sens que sur un toggle button persistant -->
<a href="/actualites" role="button" aria-pressed="false">
  Actualités
</a>
→ NVDA : "Actualités, bouton bascule non activé, lien"

aria-pressed signale un état persistant (activé / désactivé). Sur un lien de navigation, ça n’a aucun sens, la page n’est pas “activée” ou “désactivée”. L’utilisateur AT est induit en erreur.

Correction :

<!-- ✓ Lien de navigation : juste un <a>, avec aria-current si page active -->
<a href="/actualites" aria-current="page">
  Actualités
</a>

Exemple : aria-live sur chaque div modifié

<!-- ✗ aria-live sur un composant qui change rarement et de façon prévisible -->
<div aria-live="polite" class="carte-produit">
  <h2>Chaise ergonomique</h2>
  <p class="prix">249 €</p>
</div>

Le prix change si l’utilisateur sélectionne une variante, mais aria-live sur toute la carte annonce chaque changement, y compris les mises à jour de titre et de description. L’utilisateur AT reçoit un flux d’annonces non sollicitées.

Correction : aria-live uniquement sur la zone qui change et doit être annoncée.

<div class="carte-produit">
  <h2>Chaise ergonomique</h2>
  <!-- Seul le prix est une live region -->
  <p class="prix" aria-live="polite">249 €</p>
</div>

Critère RGAA : 7.1 .

Anti-pattern 2 : La surcharge ARIA

Description : ajouter des attributs ARIA redondants avec la sémantique HTML native. Ça n’améliore rien, mais ça alourdit le code et signale une incompréhension de la relation HTML/ARIA.

<!-- ✗ Tout redondant — HTML natif fait déjà tout ça -->
<nav role="navigation" aria-label="navigation">
  <ul role="list">
    <li role="listitem">
      <a href="/" role="link">Accueil</a>
    </li>
  </ul>
</nav>

<h2 role="heading" aria-level="2">Nos services</h2>

<button type="button" role="button" aria-disabled="false">
  Envoyer
</button>

<input type="checkbox" role="checkbox" aria-checked="false">

Ces redondances ne créent pas d’erreur fonctionnelle, mais révèlent une méconnaissance des éléments HTML sémantiques. En audit, elles indiquent souvent qu’il y a d’autres problèmes ARIA plus graves ailleurs dans le code.

Correction : supprimer les attributs redondants. La règle 1 de WAI-ARIA s’applique : ne pas utiliser ARIA si HTML suffit.

<!-- ✓ HTML natif — aucun ARIA nécessaire -->
<nav aria-label="Navigation principale">
  <ul>
    <li><a href="/">Accueil</a></li>
  </ul>
</nav>

<h2>Nos services</h2>
<button>Envoyer</button> <!-- hors form : type="button" implicite -->
<input type="checkbox">
Note : type="button" est implicite hors <form>. Le préciser explicitement est une convention défensive utile si le bouton risque d’être déplacé dans un formulaire, pas une exigence RGAA.

Exception légitime : role="list" sur <ul> avec list-style: none : certains navigateurs (Safari) suppriment le rôle liste quand les puces sont masquées en CSS.

Critère RGAA : 8.9 .

Anti-pattern 3 : aria-hidden sur des éléments focusables

Description : appliquer aria-hidden="true" sur un élément interactif ou sur un ancêtre contenant des éléments interactifs.

C’est l’erreur la plus grave de cette liste, elle crée des pièges clavier silencieux.
<!-- ✗ Catastrophe 1 — bouton focusable masqué aux AT -->
<button aria-hidden="true" onclick="fermerModal()">
  <svg><!-- icône × --></svg>
</button>

<!-- ✗ Catastrophe 2 — navigation entière masquée, liens toujours focusables -->
<nav aria-hidden="true">
  <ul>
    <li><a href="/">Accueil</a></li>
    <li><a href="/contact">Contact</a></li>
  </ul>
</nav>

<!-- ✗ Catastrophe 3 — modal accessible mais fond non masqué -->
<main>
  <h1>Contenu principal</h1>
  <a href="/infos">En savoir plus</a>
  <!-- Le focus peut atteindre ce lien pendant que la modale est ouverte -->
</main>
<div role="dialog" aria-modal="true">...</div>

Dans le cas 1, l’utilisateur clavier atteint le bouton (Tab le sélectionne), mais le lecteur d’écran n’annonce rien. Piège silencieux : l’utilisateur est bloqué sans comprendre pourquoi.

Dans le cas 2, les liens sont toujours dans l’ordre de tabulation mais muets pour les AT.

Ce que NVDA annonce dans le cas 1 :

[Tab sur le bouton masqué]
→ [silence] — l'élément reçoit le focus mais rien n'est annoncé

Correction :

<!-- ✓ Cas 1 — aria-hidden uniquement sur l'icône décorative, pas sur le bouton -->
<button onclick="fermerModal()">
  <svg aria-hidden="true" focusable="false"><!-- icône × --></svg>
  <span class="sr-only">Fermer</span>
</button>

<!-- ✓ Cas 2 — si la navigation doit être masquée aux AT, utiliser display:none ou hidden -->
<nav hidden>...</nav>

<!-- ✓ Cas 3 — pendant l'ouverture de la modale, rendre le fond inerte -->
<main inert>...</main>
<div role="dialog" aria-modal="true">...</div>

Règle absolue : aria-hidden="true" ne doit jamais être appliqué sur un élément avec tabindex ≥ 0, sur <a href>, <button>, <input>, <select>, <textarea>, ni sur aucun ancêtre de ces éléments.

Critères RGAA : 7.1 , 7.3 .

Anti-pattern 4 : role="menu" sur la navigation principale

Description : utiliser role="menu" et role="menuitem" pour le menu de navigation d’un site. C’est l’anti-pattern le plus répandu sur les sites WordPress avec des thèmes personnalisés.

<!-- ✗ Navigation principale avec rôles de menu applicatif -->
<nav>
  <ul role="menu">
    <li role="menuitem"><a href="/">Accueil</a></li>
    <li role="menuitem"><a href="/mairie">Mairie</a></li>
    <li role="presentation">
      <a role="menuitem" href="/services">Services</a>
      <ul role="menu">
        <li role="menuitem"><a href="/services/urbanisme">Urbanisme</a></li>
      </ul>
    </li>
  </ul>
</nav>
→ NVDA (mode navigation) : annonce "menu", l'utilisateur s'attend à naviguer aux flèches
→ Flèche bas : aucun comportement (les flèches ne fonctionnent que si JavaScript implémente le pattern menu)
→ Tab : navigue entre les items mais NVDA annonce "menuitem", confusion totale

role="menu" est réservé aux menus d’application (Fichier, Édition, Affichage dans un logiciel). Il impose un comportement clavier très spécifique (navigation aux flèches, pas à Tab) que les utilisateurs n’attendent pas sur une navigation de site.

Si le JavaScript n’implémente pas ce comportement, l’utilisateur clavier se retrouve avec un composant qui promet une navigation aux flèches mais ne la fournit pas.

Correction :

<!-- ✓ Navigation de site : pas de rôle ARIA sur ul/li -->
<nav aria-label="Navigation principale">
  <ul>
    <li><a href="/" aria-current="page">Accueil</a></li>
    <li><a href="/mairie">Mairie</a></li>
    <li>
      <a href="/services"
         aria-haspopup="true"
         aria-expanded="false"
         aria-controls="sous-menu-services">
        Services
      </a>
      <ul id="sous-menu-services">
        <li><a href="/services/urbanisme">Urbanisme</a></li>
      </ul>
    </li>
  </ul>
</nav>

Critères RGAA : 7.1 , 7.3 , 9.2 .

Anti-pattern 5 : Live regions mal utilisées

Trois erreurs distinctes, souvent combinées.

5a : Création et injection simultanées

<!-- ✗ La zone n'existait pas avant — les AT manquent l'annonce -->
<script>
  const alerte = document.createElement('div');
  alerte.setAttribute('role', 'alert');
  alerte.textContent = 'Formulaire envoyé avec succès.';
  document.body.appendChild(alerte); // créé ET rempli en même temps
</script>

La plupart des lecteurs d’écran surveillent les live regions existantes. Une zone créée et remplie simultanément n’est pas surveillée au moment de l’injection : le message est manqué.

Correction :

<!-- ✓ Zone pré-existante dans le HTML, vide au chargement -->
<div role="status" id="confirmation" aria-live="polite"></div>

<script>
  // Vider puis remplir pour forcer la relecture
  const zone = document.getElementById('confirmation');
  zone.textContent = '';
  requestAnimationFrame(() => {
    zone.textContent = 'Formulaire envoyé avec succès.';
  });
</script>

5b : role="alert" ou aria-live="assertive" sur tout

<!-- ✗ Assertive sur une confirmation non urgente -->
<div role="alert">
  Vos préférences ont été enregistrées.
</div>

<!-- ✗ Assertive sur un compteur de résultats -->
<div aria-live="assertive" id="nb-resultats">
  42 résultats trouvés
</div>

role="alert" et aria-live="assertive" interrompent la lecture en cours quelle qu’elle soit. Utilisés pour des confirmations banales, ils perturbent l’expérience AT sans justification. L’utilisateur qui était en train de lire un paragraphe se retrouve interrompu pour entendre “Vos préférences ont été enregistrées.”

Correction :

<!-- ✓ Polite pour les confirmations non urgentes -->
<div role="status" aria-live="polite">
  Vos préférences ont été enregistrées.
</div>

<!-- ✓ Réserver assertive/alert aux erreurs et urgences -->
<div role="alert">
  Erreur : impossible de se connecter au serveur.
</div>

5c : Live region sur un compteur haute fréquence

<!-- ✗ Le compteur se met à jour toutes les secondes -->
<div aria-live="polite" role="timer" id="chrono">01:23</div>

NVDA annonce “01:23” toutes les secondes : flux ininterrompu d’annonces qui rend la page inutilisable.

Correction :

<!-- ✓ aria-live="off" sur les éléments à haute fréquence de mise à jour -->
<div aria-live="off" role="timer" id="chrono">01:23</div>
<!-- L'utilisateur peut lire la valeur quand il navigue dessus, sans annonce automatique -->

Critère RGAA : 7.5 .

Anti-pattern 6 : aria-label qui contredit le texte visible

Description : utiliser aria-label avec une valeur différente du texte visible. Crée une discordance entre ce que voit l’utilisateur voyant et ce qu’entend l’utilisateur AT. Viole aussi WCAG 2.5.3 (Label in Name).

<!-- ✗ aria-label différent du texte visible -->
<button aria-label="Fermer la fenêtre de dialogue">
  Annuler
</button>

<!-- ✗ aria-label décrivant l'apparence -->
<button aria-label="Bouton bleu arrondi">
  Envoyer
</button>

<!-- ✗ aria-label vide — lien sans intitulé -->
<a href="/contact" aria-label="">
  Nous contacter
</a>

Dans le premier cas, l’utilisateur voyant lit “Annuler” mais l’utilisateur AT entend “Fermer la fenêtre de dialogue”. Un utilisateur vocal (Dragon Naturally Speaking) dit “cliquer Annuler”, mais la commande échoue car le bouton s’appelle “Fermer la fenêtre de dialogue” pour Dragon.

Ce que NVDA annonce :

[Tab sur le bouton]
→ "Fermer la fenêtre de dialogue, bouton"
  (le texte visible "Annuler" est complètement écrasé)

Correction :

<!-- ✓ aria-label cohérent avec le texte visible, ou absent si le texte suffit -->
<button>Annuler</button>

<!-- ✓ Si le contexte doit être précisé, l'inclure dans le texte visible -->
<button>
  Annuler
  <span class="sr-only">la réservation en cours</span>
</button>

<!-- ✓ aria-label uniquement quand il n'y a pas de texte visible -->
<button aria-label="Fermer">
  <svg aria-hidden="true" focusable="false"><!-- icône × --></svg>
</button>

Cas particulier d’aria-label en complément : si tu dois enrichir le label pour le rendre explicite hors contexte, inclure le texte visible dans aria-label :

<!-- Le texte visible est "En savoir plus" — insuffisant hors contexte -->
<!-- aria-label reprend et enrichit le texte visible -->
<a href="/mairie/conseil" aria-label="En savoir plus sur le conseil municipal">
  En savoir plus
</a>

Critères RGAA : 6.1 , 6.2 , 11.1 , 11.2 .

Anti-pattern 7 : tabindex positif

Description : utiliser des valeurs positives de tabindex (tabindex="1", tabindex="2"…) pour forcer un ordre de navigation. En théorie ça semble contrôler l’ordre — en pratique ça le brise.

<!-- ✗ tabindex positifs — ordre de tabulation chaotique -->
<header>
  <a href="/" tabindex="1">
    <img src="logo.png" alt="Accueil">
  </a>
  <nav tabindex="2">
    <a href="/mairie">Mairie</a>
    <a href="/contact">Contact</a>
  </nav>
</header>
<main>
  <h1>Bienvenue</h1>
  <a href="/actualites" tabindex="3">Dernières actualités</a>
  <p>Texte de présentation avec <a href="/histoire">un lien</a>.</p>
</main>

L’ordre de tabulation avec ces tabindex :

  1. Logo (tabindex=“1”)
  2. Le <nav> lui-même (tabindex=“2”), pas ses liens enfants
  3. Lien “Dernières actualités” (tabindex=“3”)
  4. Puis tous les éléments sans tabindex, dans l’ordre du DOM : “Mairie”, “Contact”, “un lien”

Le lien “un lien” dans le texte de présentation arrive après les actualités mais avant les liens de navigation — complètement contre-intuitif.

Règle : tabindex positif crée un ordre secondaire qui est toujours parcouru avant les éléments naturels (tabindex=“0” ou sans tabindex). Dès qu’un seul élément a tabindex="1", il passe en premier, même s’il est en bas de page.

Correction : ne jamais utiliser de tabindex positif. Si l’ordre de tabulation est mauvais, corriger l’ordre du DOM.

<!-- ✓ Pas de tabindex positif — ordre naturel du DOM -->
<!-- tabindex="0" uniquement pour rendre focusable un élément non interactif -->
<!-- tabindex="-1" uniquement pour retirer du flux de tabulation -->

Critère RGAA : 12.8 .

Anti-pattern 8 : Confusion bouton / lien

Description : utiliser le mauvais élément HTML pour l’action, ou patcher avec ARIA au lieu de corriger la structure.

<!-- ✗ Lien utilisé comme bouton d'action -->
<a href="#" onclick="soumettreFormulaire()">
  Envoyer
</a>

<!-- ✗ Bouton utilisé pour naviguer -->
<button onclick="window.location.href='/contact'">
  Nous contacter
</button>

<!-- ✗ Lien patché avec role="button" -->
<a href="/telecharger-pdf" role="button">
  Télécharger le document
</a>

La règle sémantique :

  • <a href> : navigation vers une URL, une ancre, ou un téléchargement. Activé avec Entrée uniquement.
  • <button> : déclenchement d’une action JavaScript. Activé avec Entrée ET Espace.

Quand un lien a role="button", NVDA annonce “bouton”, l’utilisateur presse Espace pour l’activer (comportement attendu d’un bouton). Mais Espace sur un <a> fait défiler la page, pas activer le lien. L’utilisateur AT ne peut pas activer le “bouton”.

Ce que NVDA annonce et ce qui se passe :

[Tab sur <a role="button">Télécharger le document</a>]
→ "Télécharger le document, bouton" ← NVDA dit "bouton"

[Espace] → la page défile ← comportement de lien, pas de bouton
[Entrée] → navigation vers /telecharger-pdf ← seule activation possible

Correction :

<!-- ✓ Lien pour navigation et téléchargement -->
<a href="/telecharger-pdf" download>
  Télécharger le document
  <span class="sr-only">(PDF, 2 Mo)</span>
</a>

<!-- ✓ Bouton pour action JS -->
<button type="button" onclick="soumettreFormulaire()">
  Envoyer
</button>

<!-- ✓ Si la destination est une URL ET que c'est une action principale -->
<!-- Utiliser un lien stylisé en bouton — sans role="button" -->
<a href="/contact" class="btn-primaire">
  Nous contacter
</a>

Critères RGAA : 6.1 , 7.1 , 7.3 .

Anti-pattern 9 : aria-required sans gestion d’erreur

Description : ajouter aria-required="true" pour signaler les champs obligatoires, mais ne pas implémenter aria-invalid et aria-errormessage lors de la validation. L’utilisateur est prévenu de l’obligation mais pas de l’erreur.

<!-- ✗ aria-required présent mais validation incomplète -->
<label for="email">Email *</label>
<input
  type="email"
  id="email"
  aria-required="true"
>
<!-- Après soumission avec champ vide : aucun feedback AT -->
[Soumission avec champ vide]
→ [visuellement] un message rouge apparaît sous le champ
→ [NVDA] ... silence. Le champ n'est pas annoncé comme invalide.

Correction : les trois attributs vont toujours ensemble lors d’une erreur :

<label for="email">Email <span aria-hidden="true">*</span></label>
<input
  type="email"
  id="email"
  aria-required="true"
  aria-invalid="true"
  aria-describedby="erreur-email"
>
<p id="erreur-email" role="alert">
  Saisissez une adresse email valide (ex. : nom@exemple.fr).
</p>
[Focus sur le champ après soumission]
→ "Email, zone de saisie, invalide, requis, Saisissez une adresse email valide…"

[role="alert" annonce immédiatement]
→ "Saisissez une adresse email valide (ex. : nom@exemple.fr)."

Critères RGAA : 11.9 , 11.10 , 11.11 .

Anti-pattern 10 : placeholder comme seul label

Description : utiliser l’attribut placeholder comme unique étiquette visible d’un champ, sans <label> associé. Le placeholder disparaît dès la saisie — l’utilisateur perd l’information sur ce qu’il est en train de remplir.

<!-- ✗ Placeholder comme seul label -->
<input type="text" placeholder="Votre nom">
<input type="email" placeholder="Votre adresse email">
<input type="tel" placeholder="Votre téléphone">

<!-- ✗ aria-label avec la même valeur que placeholder — cache le placeholder -->
<input type="text" placeholder="Votre nom" aria-label="Votre nom">

Problèmes cumulés :

  1. Le placeholder disparaît à la saisie : l’utilisateur oublie ce qu’il remplissait
  2. Le contraste du placeholder est souvent insuffisant (texte grisé sur fond blanc)
  3. Certains lecteurs d’écran n’annoncent pas le placeholder comme label
  4. La traduction automatique ne traduit pas toujours les placeholder

Ce que NVDA annonce selon la configuration :

[Avec <input placeholder="Votre nom"> sans label]
→ Selon NVDA : "Votre nom, zone de saisie" (si le placeholder est lu)
   ou : "Zone de saisie" (si le placeholder est ignoré selon version NVDA)

Correction :

<!-- ✓ Label visible + placeholder complémentaire -->
<div class="champ">
  <label for="nom">Nom</label>
  <input
    type="text"
    id="nom"
    placeholder="ex. : Dupont"
    autocomplete="family-name"
  >
</div>

<!-- ✓ Si la maquette impose de cacher le label visuellement -->
<div class="champ">
  <label for="nom" class="sr-only">Nom</label>
  <input type="text" id="nom" placeholder="Nom" autocomplete="family-name">
</div>

Le placeholder a un rôle d’exemple ou d’indication de format, jamais de label.

Critères RGAA : 11.1 , 11.2 .

Récapitulatif : Les 10 anti-patterns en un coup d’œil

#Anti-patternSignal d’alerte en auditCritères
1ARIA cargo cultaria-pressed sur des liens, aria-live partout7.1
2Surcharge ARIA<nav role="navigation">, <h2 role="heading">8.9
3aria-hidden sur focusable<button aria-hidden="true">, <nav aria-hidden="true"><a>7.1, 7.3
4role="menu" sur navigation<ul role="menu"> dans <nav>7.1, 7.3, 9.2
5Live region mal utiliséeZone créée et remplie simultanément, role="alert" partout7.5
6aria-label qui contreditLabel différent du texte visible, aria-label=""6.1, 6.2
7tabindex positiftabindex="1", tabindex="2" dans le HTML12.8
8Bouton vs lien<a role="button">, <button onclick="location.href">6.1, 7.1, 7.3
9aria-required seularia-required sans aria-invalid ni message d’erreur11.9, 11.10, 11.11
10Placeholder comme label<input placeholder="Nom"> sans <label>11.1, 11.2

Comment les détecter en audit

Automatiquement (axe DevTools, WAVE) :

  • Anti-patterns 3, 6 (aria-label vide), 10 : détectés de façon fiable
  • Anti-pattern 7 : partiellement détecté

Manuellement obligatoire :

  • Anti-patterns 1, 2, 4, 5, 8 : nécessitent de lire le code et tester au clavier
  • Anti-pattern 9 : tester la soumission du formulaire avec des champs vides

En inspectant l’arbre d’accessibilité (DevTools → onglet Accessibilité) :

  • Vérifier le nom calculé de chaque élément interactif
  • Vérifier le rôle calculé (pas le rôle ARIA déclaré, le rôle réel après calcul)
  • Vérifier les états (expanded, selected, checked, invalid…)

La lettre de l'Atelier A11Y

Ressources pédagogiques, critères RGAA commentés et retours de terrain : une lettre mensuelle pour progresser sur l'accessibilité numérique, sans jargon.

  • Nouveaux articles et ressources pédagogiques
  • Critères RGAA décortiqués avec des exemples concrets
  • Bonnes pratiques et retours d'expérience terrain
S'abonner à la newsletter (s'ouvre dans un nouvel onglet)

Gratuit. Désabonnement possible à tout moment.