# Une case à cocher personnalisée invisible pour un lecteur d’écran : le correctif

> Une case à cocher stylée en CSS peut devenir totalement invisible pour un lecteur d'écran. Symptôme, diagnostic et correctif d'un piège très répandu sur les formulaires WordPress.

- Auteur : Clément Hadrot
- Publié le : 2020-03-19
- Mis à jour le : 2020-03-19
- Catégorie : Accessibilité
- URL : https://wpmoderne.dev.wordpress-developpement.fr/accessibilite/case-a-cocher-personnalisee-invisible-lecteur-ecran/

## L’essentiel

- display none masque aussi pour les lecteurs d'écran
- Utiliser un positionnement hors-écran, pas none
- Vérifier avec NVDA après chaque correctif

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

> L'essentiel à retenir : display none masque aussi pour les lecteurs d'écran ; Utiliser un positionnement hors-écran, pas none ; Vérifier avec NVDA après chaque correctif

Il existe une différence fondamentale entre « invisible à l'écran » et « invisible pour tout le monde » :

- `display: none` et `visibility: hidden` retirent 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é.
