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é JavaScriptindeterminate, que le navigateur restitue automatiquement commearia-checked="mixed".
Critères RGAA applicables
- Critère 11.1 : étiquette de champ. Une case à cocher est un champ de formulaire et doit avoir une étiquette associée, en pratique une balise
<label>dont l'attributforest égal à l'iddu champ. - Critère 11.2 : étiquette pertinente. L'étiquette doit permettre de connaître la fonction exacte du champ, donc ce que le fait de cocher signifie concrètement.
- Critère 11.5 : regroupement de champs et critère 11.6 : légende de regroupement. Des cases à cocher de même nature doivent être regroupées (
<fieldset>ourole="group") et chaque regroupement doit porter une légende qui formule la question posée. - Critère 7.1 : compatibilité avec les technologies d'assistance. Une case à cocher recréée en script (
role="checkbox") doit exposer son nom, son rôle et ses changements d'états, y comprisaria-checked="mixed"pour l'état partiel. - Critère 7.3 : contrôle par le clavier et tout dispositif de pointage. Une case à cocher scriptée doit rester utilisable au clavier et à la souris, exactement comme l'élément natif.
- Critère 11.4 : étiquette accolée. L'étiquette d'une case à cocher doit être visuellement accolée immédiatement à droite du champ, ou en dessous, pour une langue lue de gauche à droite (test 11.4.3). C'est l'inverse des champs texte, dont l'étiquette précède le champ.
- Critère 10.7 : prise de focus visible. L'indicateur de focus doit rester visible, y compris sur une case à cocher au style personnalisé : ne jamais supprimer l'outline sans le remplacer.
Interaction clavier attendue
| Touche | Action |
|---|---|
| Espace | Bascule l'état de la case (coché, non coché et, en tri-état, partiellement coché) |
| Entrée | Ne 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érerappearance: nonesur le vrai champ. <label>non associé (pas defor/idconcordants) : 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 viasetAttribute: 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 :
- 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.
- Cliquer sur le texte de l'étiquette : la case doit basculer. Sinon, le
foret l'idne concordent pas. - 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.
- 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