L'interrupteur
Avant de coder un interrupteur, il faut savoir si c'en est un : la case à cocher exprime un choix qui sera soumis, l'interrupteur applique un effet immédiat. Démo, code, critères RGAA.
6 critères RGAA en jeu
Essayez-les
Affichage du rapport
11.1 Étiquette de champ de formulaire
10.7 Visibilité de la prise de focus
3.2 Contraste du texte
Ce que le DOM expose, relu à chaque bascule :
Tabulez sur chacun des deux interrupteurs et pressez Espace : le
rapport change aussitôt, sans rien à valider. Le second est un <button> et
répond aussi à Entrée ; le premier est une case à cocher, et
Cette démo existe aussi en page autonome.
Case à cocher, ou interrupteur ?
La question se tranche sur un seul critère, et il n’est pas graphique : quand le changement prend-il effet ?
- Une case à cocher exprime une valeur qui sera soumise. On la coche, on continue son formulaire, et rien ne se produit avant le bouton d’envoi. Elle se décoche sans conséquence tant qu’on n’a pas validé.
- Un interrupteur applique son effet à l’instant où on le bascule. Il n’y a pas de bouton d’envoi, pas de retour en arrière par annulation du formulaire : la bascule est l’action.
Le dessin ne décide de rien. Une case à cocher stylée en piste et curseur reste une case à cocher si son effet attend une validation, et le lecteur d’écran a raison de l’annoncer comme telle. Inversement, un réglage qui s’applique aussitôt reste un interrupteur même dessiné en carré à cocher.
Ce départ compte parce qu’il change ce qu’on promet. « Interrupteur, activé » dit à la personne qui l’entend que la chose est faite. Si en réalité il faut encore valider, l’annonce est un mensonge, et c’est l’utilisateur qui en paiera le prix — pas le composant.
L’erreur courante
L’interrupteur dessiné à la main, parce que la case à cocher native ne ressemblait pas à une piste.
<!-- À ne pas faire -->
<span class="intitule">Notifications</span>
<div class="interrupteur" onclick="basculer(this)">
<span class="piste"><span class="curseur"></span></span>
</div> /* À ne pas faire : l'état n'existe qu'ici, en pixels */
.interrupteur.actif .curseur {
left: auto;
right: 2px;
} Le div ne prend pas le focus, n’écoute ni Espace ni
Mais le défaut le plus profond est ailleurs. L’état ne vit que dans une classe CSS et une position en pixels. Rien dans le DOM ne dit qu’il y a quelque chose à basculer, ni que c’est activé. Une technologie d’assistance ne lit pas la feuille de style : elle lit l’arbre d’accessibilité, où ce composant n’existe pas. Et le texte « Notifications », posé à côté sans lien, n’étiquette rien — il flotte.
Trois variantes coûtent aussi cher :
- L’étiquette qui nomme l’état. « Activées » à côté de la piste donne « Activées, interrupteur, activé » : deux fois l’état, jamais l’objet. La personne sait que quelque chose est allumé, sans savoir quoi.
aria-checkedposé une fois. Écrit dans le HTML au chargement, jamais réécrit au clic. Le défaut est invisible à l’œil : le curseur se déplace, l’annonce ne bouge pas et reste figée sur la valeur initiale.- L’interrupteur dans un formulaire à valider. Il annonce un effet immédiat et n’en produit aucun tant qu’on n’a pas cliqué « Enregistrer ». Deux cases à cocher auraient dit la vérité.
Le code, pas à pas
1. La case à cocher, requalifiée
C’est le point de départ, parce que c’est celui qui a le moins à écrire :
<label for="reglage-notifications">Notifications</label>
<input type="checkbox" role="switch" id="reglage-notifications" /> L’<input> garde tout ce que le navigateur donne : le focus, Espace,
la mise à jour de checked, l’association au <label for>. Le role="switch" ne change qu’une chose — le mot restitué, « interrupteur » au lieu de « case à
cocher ». Aucun comportement n’est à réécrire.
Le dessin se fait par appearance: none sur l’input lui-même, comme pour une case à cocher : la piste devient
le fond de l’élément, le curseur un ::before. Ce que cette technique préserve,
et qui compte : le contrôle reste dans la page, focalisable et restitué. Le
masquer par display: none pour dessiner à côté le retirerait de l’ordre de
tabulation en même temps que de l’écran.
2. Le bouton, quand le dessin l’exige
Si la piste demande plus que ce qu’un <input> accepte, le second motif prend
le relais :
<button type="button" role="switch" aria-checked="false" id="bascule-notifications">
<span>Notifications</span>
<span class="piste" aria-hidden="true"></span>
</button> bouton.addEventListener('click', () => {
const active = bouton.getAttribute('aria-checked') === 'true';
bouton.setAttribute('aria-checked', String(!active));
appliquer(!active);
}); Trois différences avec le motif précédent, et il faut les assumer toutes les
trois. aria-checked est à écrire soi-même, et à réécrire à chaque bascule.
L’intitulé se place dans le bouton, parce que le nom accessible d’un bouton
vient de son contenu. Et la piste, qui n’est plus qu’un dessin, porte aria-hidden="true" pour ne pas s’ajouter à ce nom.
Une chose que role="switch" n’admet pas, contrairement à role="checkbox" :
la valeur aria-checked="mixed". Un interrupteur n’a que deux positions. S’il
vous en faut une troisième — « hériter du réglage global », par exemple — c’est
une liste de choix qu’il vous faut, pas un interrupteur.
3. L’étiquette nomme ce qui est commandé
C’est le piège propre à ce composant, et il vient du vocabulaire : on parle d’un interrupteur « activé », alors on écrit « Activées » à côté.
<!-- À ne pas faire : l'étiquette redouble l'état -->
<label for="notifs">Activées</label>
<input type="checkbox" role="switch" id="notifs" />
<!-- L'étiquette nomme l'objet, le contrôle annonce l'état -->
<label for="notifs">Notifications par courriel</label>
<input type="checkbox" role="switch" id="notifs" /> Le critère 11.2 demande que l’étiquette permette de connaître la fonction exacte du champ. « Activées » ne la donne pas : elle décrit une position, que le contrôle restitue déjà de son côté. Résultat, l’annonce répète l’état deux fois et ne nomme jamais l’objet.
La règle qui en découle tient en une phrase : l’étiquette d’un interrupteur ne doit pas changer quand on le bascule. Si vous êtes tenté de la réécrire — « Activer » puis « Désactiver » —, c’est que vous avez affaire à un bouton d’action, pas à un interrupteur. Les deux existent, et ils n’ont ni le même rôle ni le même contrat : un bouton qui change d’intitulé casse aussi la commande vocale, qui cherche l’intitulé qu’elle voit à l’écran.
4. Où se place l’étiquette
Voici la contradiction qu’il faut connaître avant de se la faire opposer en
audit. Le test 11.4.3 nomme explicitement role="switch", aux côtés des
cases à cocher et des boutons radio, et demande l’étiquette « immédiatement
au-dessous ou à droite » du champ. Or la disposition la plus répandue dans les
panneaux de réglages place l’étiquette à gauche, la piste alignée au bord droit
— la position que ce test n’accepte pas.
Le critère règle lui-même le conflit, par deux de ses cas particuliers : le test est non applicable lorsque le champ « n’est pas visuellement présenté sous forme de bouton radio ou de case à cocher » — ce qu’une piste et un curseur ne sont jamais —, et lorsque le contexte rend une autre position légitime du point de vue de l’expérience utilisateur. Un panneau de réglages relève des deux.
Ce qui reste exigible, et qui n’est pas dispensé : l’étiquette doit être accolée au champ (test 11.4.1) — « visuellement proches de manière à ce que la relation entre les deux ne puisse pas prêter à confusion », dit le glossaire. Une étiquette à gauche et une piste à l’autre bout d’un panneau large, avec deux autres réglages entre les deux, échoue à ce test-là — celui qui, lui, ne connaît pas de cas particulier.
5. L’état doit se voir sans la couleur
/* À ne pas faire : seule la teinte de la piste distingue les deux états */
.piste { background: var(--gris); }
[aria-checked='true'] .piste { background: var(--vert); } Le test 3.1.4 vise les propriétés CSS qui portent une information. Un interrupteur dont la piste change de couleur et rien d’autre en est un cas d’école : qui ne distingue pas le vert du gris ne sait pas dans quelle position il est.
Le déplacement du curseur suffit à régler la question — c’est une différence de forme, pas de teinte. Encore faut-il qu’il se voie : un curseur qui parcourt trois pixels ne donne pas d’information. Certaines implémentations ajoutent un symbole dans la piste, une barre et un cercle par exemple ; c’est une bonne pratique, pas une exigence du référentiel, dès lors que la position du curseur fait déjà la distinction.
Au clavier
| Touche | Effet |
|---|---|
| Tab | Passe à l’interrupteur suivant. Chacun est un arrêt de tabulation, comme chaque case à cocher. |
| Espace | Bascule l’interrupteur qui a le focus, et applique l’effet aussitôt. Le focus ne bouge pas. |
| Entrée | Bascule un interrupteur construit sur <button>. Sans effet sur une case à cocher — dans un formulaire, elle soumet. |
| ↑ ↓ ← → | Rien. Un interrupteur est un contrôle isolé, pas un membre d’un groupe où l’on choisit. |
L’écart sur Entrée entre les deux implémentations est réel et n’a pas d’incidence : le motif ARIA n’exige qu’Espace, que les deux honorent. Ce serait une raison de plus de préférer la case à cocher si vous en placez plusieurs dans un même formulaire, pour que toutes se comportent pareil.
Ce que le lecteur d’écran restitue
Le nom, la nature, l’état — de l’ordre de « Notifications par courriel, interrupteur, activé ». Après la bascule, seul l’état se réannonce : « désactivé ». La formulation varie d’un lecteur à l’autre ; les trois informations, elles, doivent y être.
Si vous entendez « Notifications, bouton » sans état, aria-checked manque ou role="switch" n’a pas été posé. Si l’état ne bouge pas alors que le curseur se
déplace, l’attribut est écrit au chargement et jamais réécrit — il ne se voit
pas sans lecteur d’écran. Si vous entendez
« Activées, interrupteur, activé », l’étiquette nomme l’état au lieu de
l’objet. Et si vous n’entendez qu’« interrupteur, activé » sans nom, la piste
est seule : le texte à côté n’est lié à rien.
Checklist
- Effet immédiat : sinon, c’est une case à cocher, et il faut l’annoncer comme telle.
<input type="checkbox" role="switch">d’abord ;<button role="switch">seulement si le dessin l’exige.aria-checkedréécrit à chaque bascule, jamais posé une fois pour toutes.- Une étiquette qui nomme l’objet — « Notifications » —, et qui ne change pas quand on bascule.
- L’étiquette accolée au champ : visuellement proche, quelle que soit sa position.
- Le curseur se déplace visiblement : l’état ne tient pas à la seule couleur.
- Deux positions, jamais trois :
aria-checked="mixed"n’existe pas pour un interrupteur.
Les critères RGAA en jeu
- Critère 7.1 — Compatibilité des scripts
Aucun élément HTML ne fait un interrupteur : son état ne vit que dans checked ou aria-checked. Posé au chargement puis jamais réécrit, il annonce l'inverse de ce que le curseur montre.
- Critère 11.1 — Étiquette de champ de formulaire
Le glossaire range role="switch" parmi les champs de formulaire : un interrupteur doit donc porter une étiquette, et la piste dessinée à côté d'un texte flottant n'en est pas une.
- Critère 11.2 — Pertinence de l’étiquette
L'étiquette doit donner la fonction exacte du champ. « Activées » décrit l'état, que le contrôle annonce déjà ; ce qu'il faut nommer, c'est ce que l'interrupteur commande.
- Critère 11.4 — Étiquette accolée à son champ
Le test 11.4.3 vise nommément role="switch" et attend l'étiquette au-dessous ou à droite, là où les panneaux de réglages la placent couramment à gauche. C'est un cas particulier écrit du critère qui règle la contradiction, pas une tolérance à supposer.
- Critère 3.1 — Information donnée par la couleur
Une piste qui ne fait que changer de teinte donne l'état par la seule couleur — c'est le test 3.1.4, sur les propriétés CSS porteuses d'information. Le déplacement du curseur est ce qui sauve le composant, à condition qu'il se voie.
- Critère 7.3 — Contrôle des scripts au clavier
Le div stylé en interrupteur ne prend pas le focus et n'écoute pas Espace : le réglage devient impossible à changer au clavier, et aucun autre contrôle de la page ne le remplace.
Vérifiez votre composant avec l'extension
Pour aller plus loin
- Les cases à cocher et les boutons radio : l’autre réponse à une question binaire, celle qui attend une validation.
- « Champ de saisie de formulaire » au glossaire :
la liste qui range
role="switch"parmi les champs de formulaire, et qui en exclutrole="button"— c’est elle qui décide si le thème 11 s’applique. - Critère 11.4 — Étiquette accolée à son champ : les cas particuliers qui règlent la place de l’étiquette d’un interrupteur.
- Critère 3.1 — Information donnée par la couleur : ce que vérifie un audit sur une piste qui ne change que de teinte.
- L’accordéon : l’autre bouton qui porte un état, avec
aria-expandedcette fois.
Sources
- ARIA APG — Switch Pattern — le contrat de comportement, et les deux positions
- MDN — ARIA
switchrole — le rôle, et pourquoimixedn’y est pas admis - WCAG — Understanding 4.1.2 Name, Role, Value — pourquoi l’état doit être exposé
- WCAG — Understanding 2.5.3 Label in Name — pourquoi l’intitulé visible doit se retrouver dans le nom accessible
- Inclusive Components — Toggle Buttons — la comparaison entre bouton bascule et interrupteur