Boutons radio accessibles : critères RGAA, clavier et ARIA
Groupe de boutons radio accessible : name partagé, fieldset et legend, navigation aux flèches, un seul arrêt de tabulation et critères RGAA applicables.
Un groupe de boutons radio propose un ensemble d'options dont une seule peut être sélectionnée à la fois. L'élément natif <input type="radio"> avec un attribut name partagé est à préférer systématiquement : c'est le name commun qui constitue le groupe, et le navigateur fournit alors gratuitement l'exclusion mutuelle, l'unique arrêt de tabulation, la navigation par flèches et le bouclage. Autrement dit, toutes les parties difficiles du motif, celles qui disparaissent en premier dans une implémentation <div>.
Quand ce motif est le mauvais choix
- Plusieurs réponses peuvent être vraies en même temps : utiliser des cases à cocher, pas des boutons radio. Des radios rendent la question sans réponse correcte possible.
- « Aucune réponse » est un choix valide : un bouton radio ne peut pas être décoché par l'utilisateur. Une fois une option choisie, il n'y a aucun retour possible à l'état non répondu. Prévoir une option explicite du type « Aucune préférence ».
role="radiogroup"+role="radio"en ARIA : uniquement si l'élément natif est réellement impossible. Il faut alors réimplémenter soi-même les flèches, le roving tabindex et l'exclusion mutuelle, que le natif offrait sans une ligne de script.
Critères RGAA applicables
- Critère 11.1 : étiquette de champ. Chaque bouton radio est un champ de formulaire et doit avoir sa propre étiquette associée (
<label for>). - Critère 11.2 : étiquette pertinente. Chaque étiquette doit permettre de connaître la fonction exacte de l'option. Annoncé hors contexte, « Option 1 » ne transmet aucune information.
- Critère 11.5 : regroupement de champs et critère 11.6 : légende de regroupement. C'est le point central du motif : les boutons radio de même nature doivent être regroupés (
<fieldset>,role="radiogroup"ourole="group") et le regroupement doit avoir une légende. La légende est la question, les étiquettes sont les réponses. - Critère 7.3 : contrôle par le clavier et tout dispositif de pointage. Une implémentation scriptée doit conserver la navigation par flèches et l'unique arrêt de tabulation du groupe.
- Critère 11.4 : étiquette accolée. L'étiquette d'un bouton radio doit être visuellement accolée immédiatement à droite du champ, ou en dessous, pour une langue lue de gauche à droite (test 11.4.3).
- Critère 10.7 : prise de focus visible. L'indicateur de focus doit rester visible, y compris sur des boutons radio au style personnalisé.
Interaction clavier attendue
| Touche | Action |
|---|---|
| Tab | Entre dans le groupe sur l'option cochée, ou sur la première option si aucune n'est cochée. Le groupe entier est un seul arrêt de tabulation |
| Espace | Coche le bouton radio qui a le focus s'il ne l'est pas déjà |
| Flèche droite / Flèche bas | Déplace le focus et coche l'option suivante ; boucle de la dernière vers la première |
| Flèche gauche / Flèche haut | Déplace le focus et coche l'option précédente ; boucle de la première vers la dernière |
Ce modèle surprend souvent : on s'attend à ce que Tab visite chaque option. Il ne le doit pas. Tab entre dans le groupe, les flèches naviguent à l'intérieur, Tab en sort.
Rôles et attributs ARIA
Avec les éléments natifs, aucun attribut ARIA n'est nécessaire : le name partagé constitue le groupe et le navigateur gère l'état.
<!-- Le name="livraison" partagé EST le groupe : exclusion mutuelle,
flèches, bouclage et arrêt de tabulation unique sont natifs. -->
<fieldset>
<legend>Mode de livraison</legend>
<div>
<input type="radio" id="liv-standard" name="livraison" value="standard" checked>
<label for="liv-standard">Standard : 3 à 5 jours, gratuit</label>
</div>
<div>
<input type="radio" id="liv-express" name="livraison" value="express">
<label for="liv-express">Express : lendemain, 4,90 €</label>
</div>
<div>
<input type="radio" id="liv-retrait" name="livraison" value="retrait">
<label for="liv-retrait">Retrait en magasin : gratuit</label>
</div>
</fieldset>
Aucun JavaScript : c'est précisément l'intérêt du natif. Pour personnaliser l'apparence, styler 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 conservant un authentique <input type="radio"> pour le navigateur et les technologies d'assistance.
/* Styler le VRAI champ : il reste focusable et restitué correctement. */
input[type="radio"] {
appearance: none;
width: 1.25rem;
height: 1.25rem;
border: 2px solid #767676;
border-radius: 50%;
display: inline-grid;
place-content: center;
}
input[type="radio"]::before {
content: "";
width: 0.65rem;
height: 0.65rem;
border-radius: 50%;
transform: scale(0);
background: #0056b3;
}
/* Le point intérieur APPARAÎT à la sélection : un changement de
forme, pas seulement de couleur. */
input[type="radio"]:checked::before {
transform: scale(1);
}
/* L'indicateur de focus reste visible (critère 10.7). */
input[type="radio"]:focus-visible {
outline: 2px solid #0056b3;
outline-offset: 2px;
}
Quand « aucune réponse » est un choix légitime, le dire explicitement plutôt que de laisser le groupe sans valeur par défaut :
<!-- L'utilisateur ne peut pas décocher un radio : si « rien » est
valide, cette option doit exister en tant que telle. -->
<fieldset>
<legend>Fréquence de la lettre d'information</legend>
<div>
<input type="radio" id="freq-aucune" name="frequence" value="aucune" checked>
<label for="freq-aucune">Ne rien m'envoyer</label>
</div>
<div>
<input type="radio" id="freq-hebdo" name="frequence" value="hebdo">
<label for="freq-hebdo">Chaque semaine</label>
</div>
</fieldset>
En version ARIA (dernier recours) : role="radiogroup" sur le conteneur, nommé par aria-labelledby ou aria-label ; role="radio" et aria-checked="true"/"false" sur chaque option ; et un roving tabindex, c'est-à-dire tabindex="0" sur l'option cochée uniquement et tabindex="-1" sur toutes les autres, pour préserver l'arrêt de tabulation unique.
Défauts fréquents et impact utilisateur
- Attribut
namedifférent sur chaque radio : les boutons ne forment plus un groupe. Tous peuvent être cochés en même temps, les flèches ne font rien et chaque option devient son propre arrêt de tabulation. Visuellement rien ne change : le composant se comporte comme des cases à cocher cassées. namecodé en dur dans un composant réutilisable (React, Vue, Angular) : deux instances du composant sur la même page fusionnent silencieusement en un seul groupe, et sélectionner dans l'une désélectionne l'autre. Générer unnameunique par instance (par exemple avecuseId()).- Groupe sans
<fieldset>/<legend>: le lecteur d'écran annonce « Standard, bouton radio, 1 sur 3 » sans jamais dire de quoi il s'agit. Un sur trois quoi ? La question n'est restituée qu'aux utilisateurs voyants (critères 11.5 et 11.6). <div role="radio">avectabindex="0"sur chaque option : même en ARIA, un groupe radio est un seul arrêt de tabulation. Chaque option focusable par Tab casse le modèle de navigation attendu.- Étiquettes du type « Option 1 », « Option 2 » : hors contexte, elles ne transmettent rien à un utilisateur de lecteur d'écran (critère 11.2).
- Gestionnaire
clickau lieu dechange: le changement de sélection par les flèches, qui est le mode principal au clavier, ne déclenche jamaisclick. L'état applicatif se désynchronise en silence, et le bug passe tous les tests à la souris.
// À ne pas faire : onClick ne se déclenche pas quand la sélection
// change aux flèches. L'état React se désynchronise du DOM.
<input type="radio" onClick={() => setLivraison(option.value)} />
// À faire : onChange couvre la souris ET la navigation aux flèches.
<input type="radio" onChange={(e) => setLivraison(e.target.value)} />
Ce que les outils automatiques ne détectent pas
Les outils automatiques, notre scanner compris, signalent de façon fiable un bouton radio sans étiquette ou des identifiants dupliqués. Trois vérifications essentielles du motif leur échappent en revanche :
- L'absence de légende sur le regroupement : juger que des options répondent à une même question, et que cette question doit être portée par une légende, reste une vérification manuelle (critères 11.5 et 11.6).
- Un
namenon partagé entre les radios : le groupe cassé ressemble, pour un outil, à des champs indépendants parfaitement valides. - Le comportement réel du groupe au clavier : un seul arrêt de tabulation, flèches qui déplacent la sélection avec bouclage, état applicatif mis à jour après une sélection aux flèches.
Vérifier ce composant
Protocole manuel rapide :
- Le test du groupe, le plus important : entrer dans le groupe avec Tab. Il doit constituer un seul arrêt de tabulation. Presser ensuite les flèches : la sélection doit se déplacer et boucler. Si Tab visite chaque option ou si les flèches ne font rien, les radios ne sont pas groupés (en général un
namenon concordant). - Au lecteur d'écran : sur une option, on doit entendre la légende (« Mode de livraison : Standard, bouton radio, 1 sur 3 »). « Standard, bouton radio » seul signifie qu'il n'y a pas de légende.
- Le test des flèches : changer la sélection uniquement aux flèches, puis vérifier que l'état de l'application a bien suivi. S'il n'a pas bougé, un gestionnaire
clicka été câblé au lieu dechange. - Le focus : l'indicateur de focus doit rester visible sur chaque option, y compris avec un style personnalisé.
Deux outils gratuits du site aident à ce contrôle : le Générateur Fieldset produit le regroupement <fieldset>/<legend> prêt à adapter, et le Testeur Focus Visible permet de valider vos styles CSS :focus pour la navigation au clavier.
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