Le ticket disait simplement : « impossible de cocher la case d’acceptation des conditions générales avec NVDA ». Le formulaire fonctionnait pourtant très bien à la souris : un joli carré personnalisé, coché d’une animation de coche verte au clic. Le problème est apparu dès l’inspection du DOM : la case à cocher native, celle que le navigateur et les lecteurs d’écran savent réellement manipuler, était masquée avec display: none.
C’est un piège extrêmement courant dès qu’un designer souhaite une case à cocher qui ne ressemble pas à celle, assez rustique, que dessine chaque navigateur. La solution consiste presque toujours à masquer l’élément natif pour le remplacer visuellement par un <span> stylé. Le problème n’est pas l’intention, mais la méthode de masquage choisie.
Symptôme : la case existe dans le DOM mais reste muette
En navigant au clavier avec Tab, le focus saute la case entièrement : aucun arrêt, aucune annonce vocale. Dans l’arbre d’accessibilité de Firefox, l’élément <input type="checkbox"> apparaît carrément absent de la liste des objets accessibles, alors qu’il est bien présent dans le HTML source.
.custom-checkbox input[type="checkbox"] {
display: none;
}
.custom-checkbox .fake-box {
width: 20px;
height: 20px;
border: 2px solid #333;
display: inline-block;
}
Voilà le coupable. La propriété display: none retire l’élément de l’arbre d’accessibilité au même titre que du rendu visuel. Un lecteur d’écran ne peut pas annoncer, ni même détecter, ce qui n’existe pas dans cet arbre. Le <span> visuel, lui, n’est ni focusable ni interprété comme une case à cocher : c’est un rectangle décoratif, point final.
Diagnostic : distinguer masquage visuel et masquage total

Il existe une différence fondamentale entre « invisible à l’écran » et « invisible pour tout le monde » :
display: noneetvisibility: hiddenretirent l’élément de l’arbre d’accessibilité : c’est un masquage total.aria-hidden="true"fait la même chose, indépendamment du rendu visuel : à éviter sur un élément interactif.- Un positionnement hors du cadre visible (technique dite visually hidden) masque uniquement à l’écran, en laissant l’élément focusable et lisible par les lecteurs d’écran.
Le correctif
La correction consiste à remplacer le masquage total par un positionnement hors-écran, qui conserve l’élément natif dans le flux du clavier et de l’arbre d’accessibilité :
.custom-checkbox input[type="checkbox"] {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}
Cette combinaison de propriétés — connue sous le nom de classe utilitaire .screen-reader-text dans le cœur de WordPress — réduit l’élément à un pixel tout en le laissant réellement présent et manipulable. Le <span> décoratif reste affiché visuellement, mais il faut aussi s’assurer que le focus visuel (le contour bleu par défaut du navigateur) se reporte sur ce <span> quand l’input natif reçoit le focus, via un sélecteur :focus + .fake-box.
input[type="checkbox"]:focus + .fake-box {
outline: 2px solid #1b5fc1;
outline-offset: 2px;
}
Vérification
Après correctif, trois vérifications rapides suffisent : tabuler jusqu’à la case au clavier, appuyer sur Espace pour la cocher, puis relancer NVDA pour confirmer que l’état « coché » ou « non coché » est bien annoncé. Si le libellé associé n’est pas lu, vérifiez que le <label> utilise bien l’attribut for pointant vers l’id de l’input, ou que l’input est imbriqué dans le <label>.
Sur chaque nouveau composant de formulaire personnalisé, je pars désormais du principe que « masquer visuellement » et « supprimer » sont deux opérations différentes, et je vérifie systématiquement laquelle des deux le CSS applique réellement.
Prévention
Pour éviter que le problème ne revienne sur le prochain composant, une règle simple à ajouter aux revues de code du thème : toute recherche de display: none ou visibility: hidden ciblant un input, un button ou un a mérite une relecture attentive avant validation.
En résumé
Un carré personnalisé plus joli qu’une case à cocher native ne pose aucun problème en soi. Le risque apparaît au moment de masquer l’original : privilégiez toujours un masquage hors-écran plutôt qu’un display: none, et testez au clavier avant de considérer le composant terminé.