Chapitre 15.1 — Concevoir accessible
⏱️ TL;DR — La plupart des problèmes d’accessibilité ne naissent pas dans le code : ils naissent dans la maquette. Un contraste trop faible, un état de focus « oublié », une information portée par la seule couleur, une cible de clic minuscule — tout ça est décidé au design, et tu ne fais que le transcrire. Concevoir accessible (le « shift-left »), c’est intégrer ces contraintes dès les design tokens et les specs, pour ne plus jamais les rattraper à la fin. Corriger un contraste dans une maquette prend 5 minutes ; le corriger sur 40 écrans déjà codés prend une semaine.
🎯 Objectifs
- Comprendre le principe du shift-left : traiter l’a11y en amont, pas en correction finale.
- Intégrer le contraste dans les design tokens de couleur, pas au cas par cas.
- Exiger que le focus, les états (erreur, survol, désactivé, chargement, vide) et les cibles soient définis dans la maquette.
- Ne jamais coder une information par la couleur seule.
- Faire de la relation dev / designer une collaboration a11y, pas un ping-pong de bugs.
Le coût d’un problème dépend de quand on le trouve
Un défaut d’accessibilité coûte de moins en moins cher à corriger plus tôt on le détecte :
La leçon : le design est le meilleur endroit pour gagner de l’accessibilité. Le contraste, la taille de texte, l’ordre visuel, les états — ce sont des décisions de conception. Si elles arrivent accessibles dans ton Figma, ton code l’est presque gratuitement.
Le contraste vit dans les tokens de couleur
Le piège classique : le designer choisit une palette « à l’œil », et on découvre au test que le gris sur blanc ne passe pas les 4.5:1 exigés (texte normal, WCAG niveau AA — voir Texte, couleur & contraste). Solution : valider le contraste une fois, au niveau des design tokens, et n’utiliser que des paires validées.
/* ❌ Couleurs posées à l'œil, contraste jamais vérifié */
.badge {
color: #9aa0a6; /* gris clair */
background: #f1f3f4; /* ~1.9:1 → illisible, échoue AA */
}
/* ✅ Tokens dont les paires texte/fond sont validées ≥ 4.5:1 */
:root {
--color-text: #1f2933; /* sur --surface : ~13:1 */
--color-text-muted: #52606d; /* sur --surface : ~7:1 */
--color-surface: #ffffff;
--color-danger-text: #b3261e; /* sur --surface : ~5.9:1 */
}
.badge {
color: var(--color-text-muted);
background: var(--color-surface);
}Documente, à côté de chaque token, sur quel fond il est garanti lisible. Un token de couleur sans son ratio de contraste est une information incomplète.
💡 Réflexe — Ne raisonne jamais « quelle couleur ? » mais « quelle paire texte / fond, et à quel ratio ? ». Le contraste n’est pas une propriété d’une couleur isolée : c’est une propriété d’un couple. Range tes tokens par paires validées et interdis les combinaisons libres. Tu élimines des dizaines de non-conformités d’un coup.
Le focus fait partie du design, pas du hasard
Trop de maquettes ne montrent que l’état « au repos ». Le navigateur fournit un focus par défaut ; les resets CSS le suppriment souvent (outline: none), et personne ne le redessine. Résultat : une interface inutilisable au clavier visible (voir Clavier & focus).
Exige que la maquette définisse un style de focus réutilisable, porté par un token, et applique-le partout via :focus-visible.
:root {
--focus-ring: 0 0 0 3px #1a73e8; /* anneau visible, contrasté sur le fond */
}
/* ✅ Un anneau de focus cohérent sur tous les éléments interactifs */
:where(a, button, input, select, textarea, [tabindex]):focus-visible {
outline: none;
box-shadow: var(--focus-ring);
}L’information ne passe jamais par la couleur seule
Un champ en erreur juste bordé de rouge, un statut « en ligne » juste en pastille verte, un lien juste en bleu sans soulignement : une personne daltonienne (ou en plein soleil) ne perçoit rien. Ajoute toujours un second signal : icône, texte, forme, soulignement.
{/* ❌ L'erreur n'existe que par la couleur du contour */}
<input className="input input--error" />
{/* ✅ Couleur + icône + message texte relié au champ */}
<input aria-invalid="true" aria-describedby="email-err" className="input input--error" />
<p id="email-err" className="error">
<IconAlert aria-hidden="true" /> E-mail invalide : il manque un « @ ».
</p>Prévoir tous les états dès la maquette
Un composant n’a pas un seul visage. Le designer doit livrer, et toi réclamer, chaque état — sinon tu improvises, et l’improvisation est rarement accessible.
| État | Ce qu’il faut prévoir côté a11y |
|---|---|
| Repos / survol | Survol ne doit pas être le seul indice (voir focus). |
| Focus clavier | Anneau visible et contrasté (:focus-visible). |
| Désactivé | Contraste suffisant du texte, raison indiquée. |
| Erreur | Couleur + icône + message relié (aria-describedby). |
| Chargement | État annoncé (aria-busy, texte « Chargement… »). |
| Vide | Message clair expliquant quoi faire, pas un écran blanc. |
Cibles de clic et typographie
- Taille de cible : vise des cibles confortables (l’ordre de 44 × 44 px est une bonne cible tactile ; WCAG 2.2 introduit un minimum sur les cibles — détaillé en Mobile, responsive & mouvement). Décidé au design via l’espacement et le padding des composants.
- Typographie : taille de base lisible (autour de 16 px pour le corps), interlignage généreux, texte redimensionnable sans casse jusqu’à 200 %. Pas de texte gris pâle « pour faire épuré ».
- Espacement : l’air entre les éléments réduit la charge visuelle — c’est aussi de l’accessibilité cognitive (chapitre suivant).
⚠️ Piège — « On rendra ça accessible plus tard, à la fin. » Le « plus tard » n’arrive jamais : la a11y devient une montagne de rattrapage qu’aucun sprint n’absorbe. Un contraste ou un focus rattrapé après coup, c’est du refactoring sur du code figé, avec régressions à la clé. Le seul moment économique pour décider l’accessibilité, c’est avant de coder.
🧭 Sur A11yLearn — Avant, le designer d’A11yLearn livrait des maquettes « au repos » et l’équipe découvrait les trous en recette. On a changé le contrat : la bibliothèque Figma d’A11yLearn ne contient plus que des tokens de couleur pré-validés (chaque paire texte/fond annotée de son ratio), un token de focus unique, et chaque composant est livré avec ses six états (repos, focus, désactivé, erreur, chargement, vide). Résultat : les tickets « contraste insuffisant » et « focus invisible » ont quasiment disparu de la recette — ils sont réglés à la source, dans la maquette.
La collaboration dev / designer
L’a11y n’est ni « le problème du dev » ni « celui du designer » : c’est une responsabilité partagée avec un partage clair.
- Le designer décide : palette et contrastes, hiérarchie visuelle, style de focus, tous les états, tailles de cible, langage.
- Le dev garantit : sémantique HTML, ordre du DOM, focus au clavier, annonces aux lecteurs d’écran, respect fidèle des specs.
- Ensemble : une revue de maquette où le dev pose les questions a11y avant le développement (« l’erreur, c’est quoi le message et l’icône ? », « le focus, il ressemble à quoi ici ? »). Cinq minutes en amont valent une semaine de tickets en aval.
✏️ Exercices
Exercice 1 — Repère les décisions de design manquantes. On te donne une maquette d’un champ « Code promo » : un input et un bouton « Appliquer ». Le designer n’a fourni que l’état au repos. Liste les états et informations qui manquent pour que tu puisses coder accessible.
✅ Solution
Manquent au minimum : (1) le focus clavier (à quoi ressemble l’anneau sur l’input et le bouton) ; (2) l’état désactivé du bouton tant que le champ est vide, avec un contraste texte suffisant ; (3) l’état erreur (code invalide) — couleur + icône + message, et le texte exact du message ; (4) l’état chargement pendant la vérification (« Vérification… », aria-busy) ; (5) l’état succès (« Code appliqué : -10 % »). Sans ces specs, tu improvises, et l’improvisation passe rarement l’audit. Réclame-les avant de coder.
Exercice 2 — Corrige la palette. Un designer te donne : texte #8a8f98 sur fond #ffffff pour les libellés secondaires, et une erreur signalée uniquement par une bordure #e53935. Que corriges-tu, et comment le figes-tu pour que ça ne se reproduise pas ?
✅ Solution
Le gris #8a8f98 sur blanc tourne autour de 3:1 : il échoue les 4.5:1 exigés pour du texte normal (AA). Il faut l’assombrir (par ex. vers #52606d, ~7:1) et le stocker comme token --color-text-muted annoté de son ratio sur --color-surface. Pour l’erreur, la couleur seule ne suffit pas : ajoute une icône et un message texte relié au champ par aria-describedby, plus aria-invalid="true". Pour éviter la récidive : n’autoriser que des paires de tokens validées, et vérifier le contraste automatiquement (voir Tester l’accessibilité).
🧠 Quiz de révision
1. Qu’est-ce que le « shift-left » appliqué à l’accessibilité ?
Déplacer le traitement de l’a11y le plus en amont possible du cycle — dès la maquette et les design tokens — au lieu de la corriger en recette ou après la mise en prod. Plus un défaut est détecté tôt, moins il coûte à corriger.
2. Pourquoi valider le contraste au niveau des tokens plutôt que par écran ?
Parce que le contraste est une propriété d’une paire texte/fond, pas d’une couleur isolée. En ne validant que des paires de tokens réutilisées partout, tu élimines des dizaines de non-conformités d’un coup, au lieu de les traquer écran par écran.
3. Pourquoi ne jamais coder une information par la seule couleur ?
Parce qu’une personne daltonienne, malvoyante ou dans de mauvaises conditions de lumière ne la percevra pas. Il faut toujours un second signal : icône, texte, forme ou soulignement, en plus de la couleur (WCAG « Utilisation de la couleur »).
4. Cite trois états qu’une maquette doit fournir au-delà du « repos ».
Parmi : focus clavier, désactivé, erreur (couleur + icône + message), chargement, vide. Sans ces specs, le dev improvise, et l’improvisation est rarement accessible.
5. Qui décide du style de focus, et où doit-il être défini ?
Le designer le décide (comme un choix de conception à part entière), et il doit être défini dans la maquette puis porté par un token unique réutilisé via :focus-visible, plutôt que laissé au hasard des resets CSS.
Chapitre suivant : Un design system accessible — comment un composant partagé transforme « corriger une fois » en « accessible partout ».