Appuyer sur Espace sur un lien ne déclenche rien. Sur un bouton, ça active l’action. Ce comportement n’est pas un bug : c’est la spécification HTML. Les navigateurs ont implémenté ce comportement depuis l’origine du web. Et tous les lecteurs d’écran, toutes les aides techniques, tous les outils de test en dépendent.
Quand un développeur utilise un <div> cliquable à la place d’un <button>, ou un <a> sans href pour déclencher une action JavaScript, ce comportement disparaît.
Ce que NVDA annonce
NVDA annonce le rôle de chaque élément interactif. Un <a href> est restitué comme “lien”. Un <button> comme “bouton”. L’utilisateur entend le rôle après l’intitulé : “Envoyer, bouton” ou “Rapport annuel 2025, lien”.
Cette distinction n’est pas cosmétique. Avec Insert + F7, NVDA extrait tous les liens de la page dans une liste navigable. Les boutons n’y apparaissent pas. Un bouton implémenté avec un <a> se retrouve dans la liste des liens, ce qui fausse la navigation par liste. Un lien implémenté avec un <button> est absent de cette liste : l’utilisateur ne peut pas y accéder par ce raccourci.
La commande vocale (Dragon NaturallySpeaking, Voice Control) utilise aussi les rôles pour cibler les éléments. Prononcer “cliquer Envoyer” active le premier élément interactif portant ce label, quel que soit son rôle. Mais si le label ne correspond pas à l’intitulé visible (parce que l’aria-label a été détourné), la commande vocale échoue.
La règle de décision
Deux questions suffisent.
L’action change-t-elle l’URL ou télécharge-t-elle un fichier ? Oui : c’est un <a href>. Un lien vers une autre page, vers une ancre, vers un fichier PDF : tous utilisent <a href>.
L’action modifie-t-elle la page sans changer l’URL ? Oui : c’est un <button>. Ouvrir une modal, soumettre un formulaire, déclencher un accordéon, lancer une recherche dans la page : tous utilisent <button>.
<!-- ✅ Lien : change l'URL -->
<a href="/contact">Nous contacter</a>
<!-- ✅ Bouton : déclenche une action sur la page -->
<button type="button" onclick="ouvrirModal()">Demander un devis</button>
<!-- ✅ Bouton : soumet un formulaire -->
<button type="submit">Envoyer le message</button>
Le CTA stylisé en bouton
Un appel à l’action (“Découvrir nos offres”, “Demander un devis”) est souvent mis en forme avec une apparence de bouton : fond coloré, coins arrondis, padding généreux. Ce style visuel ne dicte pas l’élément HTML à utiliser.
Si le CTA navigue vers une autre page, c’est un <a href>. Styler un lien comme un bouton est parfaitement valide.
<!-- ✅ CTA qui navigue : <a> avec style bouton -->
<a href="/offres" class="btn btn-primary">Découvrir nos offres</a>
<!-- ✅ CTA qui déclenche une action : <button> -->
<button type="button" class="btn btn-primary" onclick="ouvrirDevis()">
Demander un devis
</button>
L’apparence visuelle ne change pas l’élément à choisir. La question est toujours : est-ce que ça navigue ou est-ce que ça agit ?
<a> sans href : une erreur silencieuse
Un <a> sans attribut href n’est pas un lien. C’est un texte cliquable avec une apparence de lien, mais sans rôle interactif. Le navigateur ne le place pas dans l’ordre de tabulation. Le lecteur d’écran ne l’annonce pas comme un lien.
<!-- ❌ <a> sans href : non focusable, aucun rôle -->
<a onclick="ouvrirModal()">Ouvrir la fenêtre</a>
→ NVDA : texte statique, pas annoncé comme lien
→ Tab : l'élément est ignoré
→ Espace/Entrée : rien
La correction est systématique : si l’action ne navigue pas, utiliser <button type="button">.
<!-- ✅ -->
<button type="button" onclick="ouvrirModal()">Ouvrir la fenêtre</button>
→ NVDA : "Ouvrir la fenêtre, bouton"
→ Tab : l'élément reçoit le focus
→ Entrée ou Espace : action déclenchée
type="button" est spécifié explicitement parce que la valeur par défaut d’un <button> sans attribut type est submit. Un bouton sans type placé dans un <form> soumet le formulaire au clic, même s’il est imbriqué plusieurs niveaux en dessous. Les composants réutilisables finissent souvent dans des contextes inattendus : préciser le type prévient les soumissions accidentelles difficiles à diagnostiquer.
Les trois valeurs possibles :
type="button": aucun comportement par défaut, seul le gestionnaire d’événements s’exécutetype="submit": soumet le formulaire parent (valeur par défaut si l’attribut est absent)type="reset": réinitialise tous les champs du formulaire parent
<div> et <span> cliquables
Un élément générique reçoit un onclick et un style de bouton. Le résultat est non focusable, sans rôle, sans support clavier.
<!-- ❌ Div cliquable : inaccessible -->
<div class="btn" onclick="valider()">Valider</div>
→ NVDA : texte statique "Valider", aucun rôle
→ Tab : sauté
→ Clavier : inopérant
Si l’élément natif est inutilisable (contrainte framework, composant hérité), la correction minimale ajoute role="button" et tabindex="0", plus un gestionnaire d’événements pour Enter et Espace :
<!-- Correction minimale si <button> est impossible -->
<div
role="button"
tabindex="0"
onclick="valider()"
onkeydown="if(event.key==='Enter'||event.key===' '){event.preventDefault();valider()}"
>Valider</div>
Cette approche fonctionne, mais elle est fragile : le développeur doit gérer manuellement tout ce que <button> offre nativement (focus, clavier, désactivation via disabled). La première règle de l’ARIA s’applique ici : utiliser l’élément HTML natif en premier.
Le piège de role="button" sur un <a>
Certains frameworks ou bibliothèques de composants ajoutent role="button" sur un <a href> pour unifier le style visuel. Ça crée un problème : le lecteur d’écran annonce l’élément comme un bouton, ce qui implique que Espace doit fonctionner. Mais un <a> natif ne répond pas à Espace pour la navigation : Espace fait défiler la page.
<!-- ❌ role="button" sur un <a> : Espace ne navigue pas -->
<a href="/contact" role="button">Nous contacter</a>
→ NVDA : "Nous contacter, bouton"
→ Utilisateur appuie Espace : la page défile, la navigation n'est pas déclenchée
→ Utilisateur appuie Entrée : navigation déclenchée (comportement lien conservé)
L’incohérence est confuse : l’annonce dit “bouton” mais le comportement est celui d’un lien. La correction est de retirer le role="button" et de laisser le <a href> fonctionner normalement.
Si l’objectif est de déclencher une action (pas une navigation), l’élément correct est <button>, pas <a> avec un rôle forcé.
Les boutons sans intitulé
Un bouton sans texte accessible est inopérable pour un lecteur d’écran et pour la commande vocale.
Le cas le plus courant : un bouton de fermeture de modal avec une croix en icône seule.
<!-- ❌ Bouton vide : aucun intitulé -->
<button type="button" class="modal-close">
<svg aria-hidden="true"><use href="#icon-close"/></svg>
</button>
→ NVDA : "bouton" sans nom
→ Dragon : impossible à cibler par la voix
L’aria-label sur le <button> fournit l’intitulé :
<!-- ✅ -->
<button type="button" class="modal-close" aria-label="Fermer la fenêtre">
<svg aria-hidden="true" focusable="false"><use href="#icon-close"/></svg>
</button>
Le même problème se pose pour les boutons d’action répétés sur une liste (“Supprimer”, “Modifier” sans contexte). L’aria-label contextualise l’action sans modifier l’intitulé visible :
<!-- ✅ Bouton contextualisé sur une liste d'articles -->
<button type="button" aria-label="Supprimer l'article Rapport annuel 2025">
Supprimer
</button>
L’aria-label doit contenir les mots de l’intitulé visible. Écrire aria-label="Effacer" sur un bouton dont le texte visible est “Supprimer” est une non-conformité au critère WCAG 2.5.3 (Label in Name).
En audit RGAA
Trois critères sont directement liés.
Critère 7.1 : chaque composant interactif a-t-il un nom, un rôle et des états corrects ?
Tester avec l’inspecteur d’accessibilité du navigateur (arbre d’accessibilité dans DevTools). Chaque lien doit avoir le rôle link, chaque bouton le rôle button. Un <div> ou <span> cliquable sans rôle est non conforme.
Critère 7.3 : chaque composant interactif est-il utilisable au clavier ?
Tester en débranchant la souris. Chaque lien et chaque bouton doivent recevoir le focus via Tab. Chaque lien doit s’activer avec Entrée. Chaque bouton avec Entrée et Espace. Un <a> sans href ne reçoit pas le focus : non conforme.
Critère 11.9 : chaque bouton de formulaire a-t-il un intitulé explicite ?
Vérifier chaque <button>, <input type="submit"> et <input type="button">. Un bouton avec icône seule et sans aria-label est non conforme. Un bouton avec aria-label en anglais quand le site est en français viole 2.5.3.
Checklist
- Les liens utilisent
<a href>et naviguent vers une URL ou un fichier - Les boutons utilisent
<button type="button">ou<button type="submit"> - Aucun
<a>sanshrefutilisé comme déclencheur d’action - Aucun
<div>ou<span>cliquable sansrole="button"ettabindex="0" - Les
<div role="button">répondent àEntréeetEspacevia un listener explicite - Aucun
role="button"posé sur un<a href>qui navigue - Tous les boutons avec icône seule ont un
aria-labelsur le<button> - Les
aria-labelde boutons contiennent les mots de l’intitulé visible (critère 2.5.3) - Les boutons d’action répétés (Supprimer, Modifier) sont contextualisés via
aria-label
Erreurs fréquentes en audit
| Erreur | Critère | Correction |
|---|---|---|
<div> ou <span> cliquable sans rôle ni focus | 7.1, 7.3 | <button type="button"> |
<a> sans href pour déclencher une action JS | 7.1, 7.3 | <button type="button"> |
role="button" sur un <a href> | 7.3 | Retirer le rôle, garder le <a href> |
Bouton icône sans aria-label | 11.9 | aria-label décrivant l’action sur le <button> |
aria-label en anglais sur un bouton français | 11.9 | Traduire en français et vérifier que l’intitulé visible est inclus |
| Boutons “Supprimer” répétés sans contexte | 11.9 | aria-label="Supprimer l'article [titre]" |
<div role="button"> sans gestionnaire keydown | 7.3 | Ajouter listener pour Enter et Space, ou passer à <button> |