Composant · Fondamentaux

Bouton accessible : nom, états ARIA et critères RGAA

Le contrat du bouton accessible : élément button natif, nom accessible des boutons icône, états aria-pressed et aria-disabled, clavier Entrée et Espace.

Un bouton déclenche une action : soumettre un formulaire, ouvrir une boîte de dialogue, supprimer un élément. L'élément natif <button type="button"> est le seul point de départ raisonnable : il fournit le focus, Entrée, Espace, le rôle et le nom sans une ligne de script. Il n'existe pratiquement aucune raison valable de construire un bouton à partir d'un <div>.

Cette fiche est le contrat de référence du motif : nom accessible, états, clavier et critères RGAA. La difficulté réelle du bouton n'est presque jamais technique, elle est sémantique (est-ce vraiment un bouton ?) et nominale (que dit-il de lui-même ?).

Quand ce motif est le mauvais choix

La distinction bouton / lien structure tout le motif :

<button><a href>
Vocationfait quelque chose sur placeva quelque part
Entréeactiveactive
Espaceactivene fait rien (défilement de page)
Ouvrir dans un nouvel ongletnonoui
Annoncé« bouton »« lien »
ExemplesEnregistrer, Supprimer, Ouvrir la modaleAccueil, Page suivante, Télécharger
  • Le contrôle navigue (change l'URL, ouvre une page) : c'est un lien <a href>, même s'il est stylé comme un bouton. Seul un lien offre l'ouverture en nouvel onglet, le clic molette et l'aperçu de destination. Un bouton qui fait location.href est annoncé « bouton » alors qu'il va quelque part (critère 6.1).
  • Le contrôle agit : c'est un bouton. Un <a href="#" onclick> est annoncé « lien » alors qu'il agit, et remonte en haut de page si le script échoue.
  • Le contrôle affiche ou masque un contenu : c'est un bouton de divulgation, avec aria-expanded (motif disclosure, distinct).
  • Le contrôle maintient un réglage à effet immédiat : voir la fiche interrupteur (role="switch").

Repère rapide : Entrée active les deux ; Espace n'active que les boutons ; « ouvrir dans un nouvel onglet » n'existe que sur les liens. Pour le pas-à-pas complet de cette décision, cas ambigus compris : Boutons vs liens : quand utiliser chaque élément HTML.

Critères RGAA applicables

Interaction clavier attendue

ToucheAction
EntréeActive le bouton.
EspaceActive le bouton. Si Espace fait défiler la page au lieu d'activer, le composant n'est pas un vrai bouton.

C'est tout, et c'est le point fort du motif : aucun modèle clavier à apprendre, à condition d'utiliser l'élément natif.

Rôles et attributs ARIA

Sur un <button> natif, aucun role n'est nécessaire. Trois règles concentrent l'essentiel du contrat :

  1. Toujours écrire l'attribut type. Dans un <form>, un <button> sans type vaut type="submit" : la source classique de soumissions accidentelles (le bouton « Afficher le mot de passe » qui envoie le formulaire).
  2. Un bouton icône seule exige un nom accessible : du texte masqué visuellement (préférable, car la commande vocale « clique Supprimer » fonctionne et le texte survit à la traduction automatique) ou un aria-label, avec aria-hidden="true" focusable="false" sur le SVG.
  3. Un seul mécanisme d'état par bouton : aria-pressed (bouton bascule) ou aria-expanded (divulgation), jamais les deux.
<!-- Bouton d'action simple : type="button" n'est pas optionnel. -->
<button type="button">Enregistrer le brouillon</button>

<!-- Soumission : dire ce que fait le bouton, pas « Valider » (critère 11.9). -->
<button type="submit">Créer mon compte</button>

<!-- Icône seule : le nom vit dans du texte masqué, l'icône est décorative. -->
<button type="button" class="bouton-icone">
  <svg aria-hidden="true" focusable="false" width="16" height="16">
    <use href="#icone-corbeille"/>
  </svg>
  <span class="sr-only">Supprimer le brouillon</span>
</button>

<!-- Bouton bascule : aria-pressed porte l'état, l'intitulé reste stable. -->
<button type="button" aria-pressed="false">
  <svg aria-hidden="true" focusable="false"><use href="#icone-gras"/></svg>
  <span class="sr-only">Mettre en gras</span>
</button>

<!-- Indisponible, mais l'utilisateur doit comprendre pourquoi :
     aria-disabled laisse le bouton focusable et annoncé. -->
<button type="button" aria-disabled="true" aria-describedby="raison-publier">
  Publier
</button>
<p id="raison-publier">Ajoutez un titre avant de publier.</p>

<!-- La navigation est un lien, même stylé en bouton. -->
<a href="/tarifs" class="bouton">Voir les tarifs</a>

aria-disabled est préférable à disabled quand l'utilisateur doit découvrir pourquoi le bouton est indisponible : disabled retire le bouton de l'ordre de tabulation (l'explication devient inatteignable), aria-disabled le laisse focusable et annoncé. En contrepartie, il n'empêche pas l'activation : c'est au script de bloquer l'action.

/* Critère 10.7 : ne jamais supprimer l'outline sans le remplacer. */
button:focus-visible,
.bouton:focus-visible {
  outline: 2px solid #0056b3;
  outline-offset: 2px;
}

/* aria-disabled n'est que sémantique : le style reste à votre charge. */
button[aria-disabled="true"] {
  opacity: 0.5;
  cursor: not-allowed;
}
// aria-disabled ne bloque PAS l'activation : c'est le prix de rester
// focusable. Garder soi-même le gestionnaire.
boutonPublier.addEventListener('click', (event) => {
  if (boutonPublier.getAttribute('aria-disabled') === 'true') {
    event.preventDefault();
    return;
  }
  publier();
});

Et les contre-exemples qui concentrent les non-conformités du motif :

<!-- À ne pas faire : un div en guise de bouton. Non focusable, sans rôle,
     sans Entrée ni Espace : il faut ensuite tabindex, role ET un
     gestionnaire clavier pour recréer <button> en moins bien. -->
<div class="bouton" onclick="enregistrer()">Enregistrer</div>

<!-- À ne pas faire : <button> sans type dans un formulaire.
     Vaut type="submit" : chaque clic envoie le formulaire. -->
<form>
  <button onclick="basculerMotDePasse()">Afficher le mot de passe</button>
</form>

<!-- À ne pas faire : un bouton qui navigue. Pas de nouvel onglet, pas de
     clic molette, annoncé « bouton » alors qu'il va quelque part
     (critère 6.1). -->
<button type="button" onclick="location.href='/tarifs'">Voir les tarifs</button>

<!-- À ne pas faire : deux mécanismes d'état cumulés.
     « Gras, bouton bascule, activé, déplié » : lequel croire ? -->
<button type="button" aria-pressed="true" aria-expanded="true">Gras</button>

<!-- À ne pas faire : un intitulé qui change avec l'état.
     « Réactiver le son, activé » : le nom et l'état se contredisent.
     Nommer la FONCTION, laisser aria-pressed porter l'état. -->
<button type="button" aria-pressed="true">Réactiver le son</button>

Dernier recours : si un élément non natif doit absolument se comporter en bouton (contrainte de framework, DOM imposé), la reconstruction complète (rôle, tabindex, Entrée, Espace) est détaillée dans Boutons ARIA personnalisés : Guide technique accessibilité.

Défauts fréquents et impact utilisateur

  • <div onclick> en guise de bouton : non focusable, sans rôle, sans Entrée ni Espace. Un utilisateur clavier ne peut ni l'atteindre ni l'activer ; un lecteur d'écran n'annonce rien d'actionnable (critères 7.1 et 7.3).
  • <button> sans type dans un formulaire : chaque clic soumet le formulaire ; l'utilisateur perd sa saisie en voulant simplement afficher son mot de passe.
  • Bouton icône sans nom accessible : annoncé « bouton » tout court ; l'utilisateur de lecteur d'écran ignore ce que le bouton fait. C'est l'un des défauts les plus répandus du web.
  • aria-label qui ne contient pas l'intitulé visible (« Envoyer le formulaire » sur un bouton affichant « Sauver ») : un utilisateur de commande vocale dit ce qu'il voit, et la commande échoue (critère 7.1, également critère 11.9 en formulaire).
  • Deux mécanismes d'état cumulés (aria-pressed et aria-expanded) : deux états annoncés, aucun compréhensible.
  • disabled sur un bouton dont une infobulle explique l'indisponibilité : le bouton n'est plus focusable, donc l'explication est inaccessible précisément quand elle est nécessaire.

Ce que les outils automatiques ne détectent pas

  • Un bouton qui devrait être un lien (ou l'inverse) : la sémantique correcte dépend de ce que fait le contrôle, ce qui relève de l'analyse humaine.
  • L'attribut type manquant dans un formulaire : le HTML est valide, seul le comportement de soumission le révèle.
  • Un intitulé qui contredit l'état (aria-pressed="true" sur un bouton nommé « Activer le son ») ou un intitulé non pertinent au sens du critère 11.9 : la pertinence est un jugement.

Les boutons icône sans nom et les onclick posés sur des éléments non interactifs sont en revanche bien détectés par les outils automatiques, dont notre scanner : c'est l'un des motifs où l'automatisation rend le plus service, sans dispenser des vérifications sémantiques ci-dessus.

Vérifier ce composant

Protocole manuel rapide, dans cet ordre :

  1. Au clavier seul : Tab atteint le bouton, Entrée ET Espace l'activent, l'indicateur de focus est visible. Si Espace fait défiler la page, ce n'est pas un vrai bouton.
  2. Le test lien / bouton : l'activation change-t-elle l'URL ? Alors le contrôle doit être un <a href> ; un clic droit doit proposer « Ouvrir dans un nouvel onglet ».
  3. Au lecteur d'écran, sur chaque bouton icône : attendre « Supprimer le brouillon, bouton », pas « bouton » tout court.
  4. À la commande vocale (ou mentalement) : prononcer l'intitulé visible doit suffire à viser le bouton ; si un aria-label l'a remplacé par autre chose, la commande échoue.

Deux outils gratuits du site aident à ce contrôle : le quiz Bouton ou Lien ? entraîne précisément le réflexe sémantique du point 2, et le Testeur Focus Visible valide vos styles CSS :focus pour la navigation au clavier (critère 10.7).

Approfondir avec les fiches critères

Vérifier ce composant sur votre site ?

Le scan repère les défauts détectables automatiquement (rôles incohérents, champs sans étiquette, attributs ARIA orphelins) ; le reste se vérifie à la main avec les protocoles de cette fiche.

Lancer un scan gratuit

Toutes les fiches composants accessibles