Composant · Formulaires et saisie

Case à cocher accessible : critères RGAA, clavier et ARIA

Rendre une case à cocher accessible : étiquette associée, fieldset et legend, tri-état indeterminate, touches clavier et critères RGAA applicables.

Une case à cocher permet de basculer entre deux états, coché et non coché, avec un éventuel troisième état « partiellement coché » (tri-état). C'est l'un des rares composants pour lesquels le HTML natif couvre la totalité du besoin : <input type="checkbox"> est focusable, bascule avec la touche Espace, est correctement restitué par les lecteurs d'écran et transmet sa valeur au formulaire. Recréer ce comportement en JavaScript revient à réimplémenter, en moins bien, ce que le navigateur offre déjà.

Quand ce motif est le mauvais choix

  • Réglage à effet immédiat : une option qui s'applique sans validation de formulaire relève de l'interrupteur (role="switch"), pas de la case à cocher. La case à cocher correspond à une valeur soumise avec le formulaire.
  • Choix mutuellement exclusifs : si une seule réponse est possible, utiliser des boutons radio, pas un groupe de cases à cocher.
  • role="checkbox" sur un <div> : pratiquement jamais justifié. Même le tri-état est couvert par l'élément natif via la propriété JavaScript indeterminate, que le navigateur restitue automatiquement comme aria-checked="mixed".

Critères RGAA applicables

Interaction clavier attendue

ToucheAction
EspaceBascule l'état de la case (coché, non coché et, en tri-état, partiellement coché)
EntréeNe doit pas basculer la case : dans un formulaire, Entrée soumet le formulaire

Si la touche Entrée bascule l'état, le composant n'est pas une vraie case à cocher : c'est le signe d'une implémentation scriptée qui s'écarte du comportement natif.

Rôles et attributs ARIA

Avec l'élément natif, aucun attribut ARIA n'est nécessaire : le navigateur dérive l'état de la propriété checked. Ne jamais poser aria-checked sur un <input type="checkbox"> natif, cela crée deux sources de vérité qui finissent par diverger.

<!-- Le regroupement porte la question (legend), les étiquettes portent
     les réponses. Aucun attribut ARIA n'est nécessaire. -->
<fieldset>
  <legend>Comment souhaitez-vous être contacté ?</legend>
  <div>
    <input type="checkbox" id="contact-email" name="contact" value="email">
    <label for="contact-email">Par e-mail</label>
  </div>
  <div>
    <input type="checkbox" id="contact-tel" name="contact" value="tel">
    <label for="contact-tel">Par téléphone</label>
  </div>
</fieldset>

Pour styler la case, conserver le vrai champ plutôt que de le masquer derrière un élément factice : appearance: none retire le rendu du système tout en gardant un authentique <input type="checkbox"> pour le navigateur et les technologies d'assistance.

/* Styler le VRAI champ : il reste focusable et restitué correctement. */
input[type="checkbox"] {
  appearance: none;
  width: 1.25rem;
  height: 1.25rem;
  border: 2px solid #767676;
  border-radius: 3px;
}

input[type="checkbox"]:checked {
  background: #0056b3;
  border-color: #0056b3;
}

/* Ne jamais retirer l'outline sans alternative visible. */
input[type="checkbox"]:focus-visible {
  outline: 2px solid #0056b3;
  outline-offset: 2px;
}

Le tri-état (« partiellement coché »)

L'état partiel se pilote par la propriété JavaScript el.indeterminate = true. Il n'existe pas d'attribut HTML indeterminate : l'écrire dans le HTML ou via setAttribute ne produit strictement rien. Cas d'usage classique, la case « tout sélectionner » :

<fieldset>
  <legend>Autorisations</legend>
  <div>
    <input type="checkbox" id="perm-toutes">
    <label for="perm-toutes">Toutes les autorisations</label>
  </div>
  <div>
    <input type="checkbox" id="perm-lecture" data-perm>
    <label for="perm-lecture">Lecture</label>
  </div>
  <div>
    <input type="checkbox" id="perm-ecriture" data-perm>
    <label for="perm-ecriture">Écriture</label>
  </div>
</fieldset>
const toutCocher = document.getElementById('perm-toutes');
const permissions = [...document.querySelectorAll('[data-perm]')];

function synchroniser() {
  const cochees = permissions.filter((p) => p.checked).length;

  // indeterminate est une PROPRIÉTÉ JavaScript, pas un attribut HTML.
  // Le navigateur la restitue automatiquement comme aria-checked="mixed".
  toutCocher.indeterminate = cochees > 0 && cochees < permissions.length;
  toutCocher.checked = cochees === permissions.length;
  // On ne touche jamais à aria-checked : le navigateur le dérive seul.
}

toutCocher.addEventListener('change', () => {
  // Cliquer une case « mixed » coche tout : l'intention est « tout me donner ».
  for (const perm of permissions) perm.checked = toutCocher.checked;
  toutCocher.indeterminate = false;
});

for (const perm of permissions) perm.addEventListener('change', synchroniser);
synchroniser();

Variante ARIA (en dernier recours)

Uniquement si le natif est réellement impossible : role="checkbox", tabindex="0", aria-checked (true, false ou mixed) tenu à jour par script, et un nom accessible (contenu texte, aria-labelledby ou aria-label). La gestion de la touche Espace, de l'état et de la valeur de formulaire est alors entièrement à votre charge.

Défauts fréquents et impact utilisateur

  • Groupe sans <fieldset>/<legend> : le lecteur d'écran annonce « Par e-mail, case à cocher » sans jamais restituer la question à laquelle ces cases répondent. Les utilisateurs voyants lisent le titre voisin ; personne d'autre ne l'entend (critères 11.5 et 11.6).
  • <fieldset> sans <legend> : le regroupement dessine une bordure mais reste anonyme. Le groupe est annoncé sans nom, ce qui ajoute du bruit sans information.
  • <div role="checkbox" onclick> : non focusable, pas de bascule avec Espace, aucun état restitué, aucune valeur transmise au formulaire. Le composant est invisible pour un utilisateur clavier ou lecteur d'écran.
  • Champ natif masqué avec un <span> décoratif à la place : le focus va sur l'élément invisible, donc l'indicateur de focus ne s'affiche sur rien de visible (critère 10.7). Préférer appearance: none sur le vrai champ.
  • <label> non associé (pas de for/id concordants) : cliquer sur le texte ne coche pas la case, et l'étiquette n'est pas restituée comme nom du champ (critère 11.1).
  • Attribut indeterminate écrit en HTML ou via setAttribute : totalement inerte, l'état partiel n'existe pas. Seule la propriété JavaScript fonctionne.
<!-- À ne pas faire : la question est un simple paragraphe, hors du
     regroupement. Elle n'est jamais annoncée avec les cases. -->
<p>Comment souhaitez-vous être contacté ?</p>
<input type="checkbox" id="c1"><label for="c1">Par e-mail</label>
<input type="checkbox" id="c2"><label for="c2">Par téléphone</label>

Ce que les outils automatiques ne détectent pas

Les outils automatiques, notre scanner compris, repèrent de façon fiable une case à cocher sans étiquette associée. En revanche, plusieurs vérifications de ce motif relèvent du jugement humain :

  • L'absence de regroupement <fieldset>/<legend> : décider que des cases sont « de même nature » et qu'une question doit être restituée demande d'interpréter le sens du formulaire (critères 11.5 et 11.6).
  • La pertinence de l'étiquette (critère 11.2) : un outil vérifie qu'une étiquette existe, pas qu'elle décrit réellement ce que cocher implique.
  • La restitution du tri-état : vérifier qu'un lecteur d'écran annonce « partiellement coché » quand une partie des enfants est cochée demande un test manuel.
  • Le comportement clavier réel (Espace bascule, Entrée soumet) sur une implémentation personnalisée.

Vérifier ce composant

Protocole manuel rapide :

  1. Au clavier seul : atteindre la case avec Tab, vérifier que Espace bascule l'état et que Entrée ne le bascule pas. L'indicateur de focus doit être visible sur l'élément que l'on voit réellement.
  2. Cliquer sur le texte de l'étiquette : la case doit basculer. Sinon, le for et l'id ne concordent pas.
  3. Au lecteur d'écran : sur une case d'un groupe, on doit entendre la légende en plus de l'étiquette (« Comment souhaitez-vous être contacté ? Par e-mail, case à cocher, non cochée »). Si seule l'étiquette est annoncée, le regroupement n'a pas de légende.
  4. Tri-état : avec une partie des cases enfants cochées, la case parente doit être annoncée « partiellement cochée » (ou « mixed »).

Deux outils gratuits du site aident à ce contrôle : le Générateur Fieldset produit un regroupement <fieldset>/<legend> valide prêt à adapter, et le Simulateur Lecteur d'Écran permet d'inspecter l'arbre d'accessibilité de la page (nom, rôle et état réellement exposés).

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