Si vous avez déjà livré une interface « polie » pour découvrir que les utilisateurs au clavier ne savent pas où ils se trouvent, vous avez rencontré la panne la plus silencieuse de l’ingénierie web.
Le site s’affiche toujours. Les métriques semblent correctes. Mais quelqu’un appuie sur Tab et l’interface devient une maison hantée : des portes s’ouvrent, les lumières sont éteintes et vous n’êtes pas sûr
dans quelle pièce vous vous trouvez.
Les états de focus sont de l’ingénierie de fiabilité pour les humains. C’est votre couche d’observabilité pour la navigation au clavier. Et oui, ils peuvent être jolis sans neutraliser
l’accessibilité ni transformer le système de design en un accident au néon.
Ce que font réellement les états de focus (et pourquoi vous devriez vous en soucier)
« Focus » est la manière dont le navigateur dit : cet élément recevra les saisies clavier si l’utilisateur tape ou active quelque chose.
Pour un utilisateur de souris, le curseur est le canal de retours. Pour un utilisateur clavier, l’indicateur de focus est le canal de retours. Le supprimer n’est pas « nettoyer l’interface » ;
c’est retirer le seul tableau de bord dont il dispose.
En termes de SRE, le style de focus ressemble au logging. Quand il est présent et clair, vous ne le remarquez pas. Quand il manque, vous passez des heures à deviner. Et comme la plupart de votre
équipe utilise un trackpad, vous ne vous en apercevrez pas jusqu’à ce que quelqu’un avec un autre mode d’entrée (uniquement clavier, dispositifs de commutation, lecteurs d’écran, utilisateurs avancés) se heurte à un mur.
Une bonne implémentation du focus fait trois choses de manière fiable :
- Il apparaît quand l’utilisateur en a besoin. Typiquement lors de la navigation au clavier, pas à chaque clic de souris.
- Il est visuellement évident. Pas « techniquement présent mais de la même couleur que l’arrière-plan ». Évident.
- Il suit l’élément interactif réel. Pas un div wrapper. Pas un enfant aléatoire. L’élément qui s’active.
Aussi : le focus n’est pas la même chose que le hover. Hover dit « votre pointeur est près de quelque chose ». Focus dit « vous êtes ici ». Traitez-le comme un état de première classe.
Quelques faits historiques utiles
Vous n’avez pas besoin de mémoriser l’histoire des standards. Mais un peu de contexte vous aide à comprendre pourquoi les navigateurs se comportent ainsi, et pourquoi certaines astuces CSS « intelligentes »
sont en réalité des bombes à retardement.
- Les premiers navigateurs affichaient des contours de focus visibles par défaut parce que le clavier était un moyen de navigation principal bien avant que les pages marketing « pixel-perfect » dominent tout.
- CSS1 n’avait pas de styles de focus sophistiqués. Le contour de focus relevait surtout de l’UA (user agent), et il était volontairement difficile à supprimer complètement par accident.
- L’époque du « outline: none » a explosé avec le flat design. Les équipes ont supprimé les contours pour coller aux maquettes, puis ont oublié de les remplacer. Le web est devenu plus silencieux et moins navigable.
- WCAG 2.0 (2008) exigeait l’accessibilité au clavier mais ne prescrivait pas une apparence de focus spécifique, donc beaucoup d’équipes ont « conformé sur le papier » tout en livrant un focus invisible.
- :focus-visible est apparu pour résoudre un vrai conflit UX : les utilisateurs voulaient un anneau de focus pour la navigation au clavier mais pas pour chaque clic de souris.
- Les navigateurs utilisent des heuristiques pour la visibilité du focus (modalité d’entrée, interaction clavier récente). C’est pourquoi votre anneau apparaît parfois « au hasard » si vous n’êtes pas conscient des règles.
- Les modes haute-contraste ont changé la donne. Les couleurs forcées au niveau du système d’exploitation peuvent outrepasser votre palette, mais les contours survivent souvent. Si vous supprimez les contours, vous supprimez l’indicateur le plus résilient.
- WCAG moderne (2.2) a renforcé les attentes sur le focus avec des exigences plus claires autour de l’apparence et de la visibilité du focus, augmentant le coût des indicateurs « minimaux ».
- Les poursuites liées à l’accessibilité ont déplacé le sujet dans les salles de direction. Le moyen le plus rapide d’obtenir un budget pour corriger le focus est, malheureusement, une lettre juridique et un client en colère.
Les trois piliers : focus-visible, liens de saut et bons contours
Si vous voulez un système de focus qui survive le trafic réel, les vrais appareils et les vrais humains, faites ces trois choses et arrêtez d’improviser :
- Utilisez
:focus-visiblepour montrer un indicateur fort lors de la navigation au clavier sans ajouter de bruit visuel aux clics de souris. - Fournissez un lien de saut afin que les utilisateurs au clavier puissent contourner la navigation répétée et atteindre rapidement le contenu principal.
- Utilisez des contours (ou des anneaux équivalents) qui ont suffisamment de contraste, ne sont pas tronqués et ne reposent pas sur des variations de couleur subtiles.
Vous pouvez vous permettre d’embellir plus tard. Si vous n’avez pas ces trois éléments, votre histoire d’accessibilité repose surtout sur des apparences.
:focus-visible : le comportement par défaut sensé
L’ancien monde était binaire : :focus s’affiche toujours, ou vous le supprimez entièrement en espérant que personne ne le remarque. Le nouveau monde utilise
:focus-visible pour afficher les indicateurs de focus uniquement quand il est probable que l’utilisateur navigue avec le clavier (ou équivalent).
La règle pratique :
:focusest l’état : l’élément est focus.:focus-visibleest l’indice de présentation : afficher l’anneau quand le navigateur pense que c’est nécessaire.
CSS de base qui fonctionne en production
C’est la partie qui fait peur aux designers et où les ingénieurs attrapent une feuille de reset. Détendez-vous. Utilisez un anneau cohérent et appliquez-le aux contrôles interactifs.
cr0x@server:~$ cat focus.css
:root {
--focus-ring-color: #1b6ef3;
--focus-ring-offset: 3px;
--focus-ring-width: 3px;
}
/* Default: no special ring on mouse click */
:where(a, button, input, select, textarea, summary, [tabindex]):focus {
outline: none;
}
/* Keyboard-visible ring */
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
outline: var(--focus-ring-width) solid var(--focus-ring-color);
outline-offset: var(--focus-ring-offset);
}
Ce que cela fait :
- Supprime le contour par défaut sur
:focusafin que les clics de souris n’affichent pas d’anneaux partout. - Ajoute un contour clair sur
:focus-visiblepour que la navigation au clavier ait toujours un repère visible.
Pourquoi le :where() ? Cela maintient la spécificité basse. Vous pouvez l’outrepasser dans le CSS des composants sans entrer dans une bataille de cascade.
Si vous pensez « mais supprimer outline est mauvais », vous avez raison en théorie. Ce n’est acceptable que si vous ajoutez un fort indicateur :focus-visible en remplacement.
Le mode d’échec est de supprimer les contours puis d’oublier le remplacement, ce qui explique comment la moitié d’internet s’est retrouvée en mode furtif pour le focus.
Vérification du support navigateur
La plupart des navigateurs modernes supportent :focus-visible. Mais les systèmes en production vivent assez longtemps pour rencontrer des clients bizarres.
Si vous avez besoin d’une solution de secours, vous pouvez styliser :focus puis supprimer l’indicateur lors d’interaction pointeur avec un petit script, ou utiliser un polyfill.
Gardez la sauvegarde minimale et testez-la. Ne construisez pas un cadre de détection de modalité personnalisé ; vous vous tromperez et vous n’aimerez pas le débogage.
Une citation pour rester honnête
L’espoir n’est pas une stratégie.
— General Gordon R. Sullivan
Cette phrase s’applique aux états de focus plus qu’on ne veut l’admettre. « Les utilisateurs utilisent probablement une souris » est de l’espoir. « Nous avons testé la navigation clavier » est une stratégie.
Liens de saut qui ne vous mettent pas dans l’embarras
Les liens de saut sont la victoire d’accessibilité la moins coûteuse que vous puissiez livrer. Ils sont aussi un excellent test de vérité : si votre organisation ne peut pas s’accorder pour ajouter un lien de saut,
vous allez avoir du mal avec tout ce qui est plus difficile.
Le lien de saut résout une douleur spécifique : la navigation répétée. Sur un site riche en contenu, l’en-tête peut contenir des dizaines d’éléments tabbables (lien logo, nav, recherche, menu utilisateur).
Les utilisateurs au clavier ne devraient pas devoir tabuler à travers tout cela à chaque page juste pour atteindre le contenu principal.
Schéma de balisage
cr0x@server:~$ cat skip-link.html
Skip to main content
Détails importants :
- Le lien de saut doit être premier ou presque premier dans l’ordre du DOM, pour qu’il soit atteignable immédiatement.
href="#main"doit cibler un élément réel qui existe sur chaque page utilisant le modèle.tabindex="-1"sur<main>permet de déplacer le focus de façon programmatique dans les navigateurs qui ne focalisent pas par défaut les éléments non interactifs.
CSS qui le cache jusqu’au focus
cr0x@server:~$ cat skip-link.css
.skip-link {
position: absolute;
top: 0;
left: 0;
padding: 0.75rem 1rem;
transform: translateY(-120%);
background: #111;
color: #fff;
z-index: 9999;
}
.skip-link:focus-visible {
transform: translateY(0);
outline: 3px solid #fff;
outline-offset: 2px;
}
C’est le bon type de « caché » : visuellement hors écran mais qui devient visible au focus. N’utilisez pas display:none ou visibility:hidden.
Ceux-ci le retirent de l’arbre d’accessibilité et de la navigation clavier. Ce n’est pas « caché », c’est « supprimé ».
Blague #1 : Le moyen le plus rapide de trouver un lien de saut manquant est de tabuler à travers un méga-menu une fois — soudainement vous devenez un acteur engagé de l’accessibilité.
Contours qui ont de l’allure (et survivent au mode sombre)
Les anneaux de focus n’ont pas à être moches. Ils doivent être visibles. Ce sont des problèmes différents.
« Le contour est moche » est souvent un prétexte pour dire « l’anneau ne correspond pas au système de design ». Très bien. Faites-le correspondre. Mais ne l’affaiblissez pas jusqu’à l’invisibilité.
Si un designer veut un anneau gris clair de 1px sur fond blanc, votre travail est de dire « non » poliment et de livrer quelque chose que les utilisateurs peuvent voir.
Outline vs box-shadow vs anneaux hybrides
Vous avez trois approches courantes :
outline: Simple, résilient, fonctionne en couleurs forcées, n’affecte pas la mise en page. Le classique pour une raison.box-shadow: Visuels plus flexibles (halo, bords flous), mais peut être coupé paroverflow:hiddenet peut disparaître en couleurs forcées.- Hybride : Utiliser
outlinepour la compatibilité forced colors, ajouterbox-shadowpour l’esthétique.
Un anneau moderne mais lisible
cr0x@server:~$ cat ring.css
:root {
--ring: #2563eb;
--ring-outer: color-mix(in srgb, var(--ring) 30%, transparent);
}
:where(a, button, input, select, textarea):focus-visible {
outline: 3px solid var(--ring);
outline-offset: 3px;
box-shadow: 0 0 0 6px var(--ring-outer);
border-radius: 6px;
}
Décisions intégrées dans ce CSS :
- Largeur 3px est généralement visible sur des arrière-plans courants.
- Offset 3px empêche l’anneau de fusionner avec les bordures et donne un aspect intentionnel.
- Un halo extérieur doux aide sur les arrière-plans animés sans exiger un anneau néon.
Mode sombre : ne vous contentez pas d’inverser les couleurs
En mode sombre, un bleu saturé peut toujours fonctionner, mais vous devez vérifier le contraste par rapport aux pixels environnants immédiats, pas seulement l’arrière-plan de la page.
Les anneaux de focus se trouvent souvent sur des cartes, puces et surfaces superposées. L’anneau doit être visible sur toutes ces surfaces.
cr0x@server:~$ cat dark-mode.css
@media (prefers-color-scheme: dark) {
:root {
--ring: #93c5fd;
}
}
Choisissez une couleur d’anneau qui fonctionne à travers les surfaces. Si votre appli utilise plusieurs élévations d’arrière-plan, envisagez un anneau à deux couleurs (intérieur + extérieur) pour garantir la visibilité.
Couleurs forcées / mode haute-contraste
Quand l’OS force les couleurs, votre palette soigneusement choisie peut être ignorée. Les contours ont plus de chances de rester visibles.
Gérez-le explicitement :
cr0x@server:~$ cat forced-colors.css
@media (forced-colors: active) {
:where(a, button, input, select, textarea):focus-visible {
outline: 2px solid CanvasText;
outline-offset: 2px;
box-shadow: none;
}
}
La décision ici est simple : en couleurs forcées, privilégiez la fiabilité plutôt que le style. Votre couleur de marque n’est pas plus importante que la capacité d’une personne à utiliser votre produit.
Composants qui cassent le focus (et comment les arrêter)
Les coupables habituels
- Boutons personnalisés construits à partir de div avec des gestionnaires de clic, sans support clavier, et sans styles de focus.
- Resets CSS trop agressifs qui suppriment les contours globalement, y compris dans les widgets tiers.
- Conteneurs avec
overflow:hiddenqui coupent les anneaux de focus basés sur box-shadow. - Modales et tiroirs qui piègent le focus de façon incorrecte ou ne restaurent pas le focus à la fermeture.
- Changements de route dans les SPA qui déplacent le contenu sans déplacer le focus, laissant les utilisateurs clavier « quelque part » dans l’ancien DOM.
N’inventez pas de nouveaux éléments interactifs
Utilisez des éléments natifs autant que possible. Un <button> vous apporte l’activation au clavier, la focalisabilité, la sémantique et le comportement gratuitement.
Recréer cela avec un div, c’est comme réimplémenter TCP parce que vous voulez « plus de contrôle ».
Gestion du focus : les règles ennuyeuses qui vous évitent des ennuis
- Lors de l’ouverture d’une modale : déplacez le focus vers la modale (généralement son titre ou le premier champ).
- Pendant que la modale est ouverte : piègez le focus à l’intérieur de la modale (cycle de l’ordre de tabulation).
- À la fermeture de la modale : restaurez le focus sur le contrôle qui l’a ouverte.
Si vous sautez l’étape de restauration, les utilisateurs clavier « tombent » hors du flux et perdent leur repère. Ce n’est pas un petit souci UX ; c’est une défaillance fonctionnelle.
Mode d’intervention rapide
Quand le focus est « cassé », les équipes ont tendance à s’agiter. Quelqu’un blâme le CSS. Quelqu’un blâme le navigateur. Quelqu’un propose une réécriture.
Ne faites pas ça. Diagnosez comme pour une régression de latence : isolez, reproduisez, identifiez la couche.
Première étape : le focus se déplace-t-il réellement ?
- Appuyez sur Tab depuis le haut de la page.
- Regardez la barre d’URL et la page ; voyez si le focus entre dans le document.
- Si rien ne semble se passer, vérifiez si la page n’engloutit pas les événements clavier.
Deuxième étape : l’indicateur manque-t-il ou est-il juste invisible ?
- Utilisez DevTools pour inspecter l’élément actuellement focalisé (
:focus). - Vérifiez les styles calculés : outline, box-shadow, changement de fond.
- Cherchez
outline: noneprovenant d’un reset ou d’une classe de base de composant.
Troisième étape : le focus est-il volé ou piégé ?
- Ouvrez et fermez des modales. Le focus revient-il au déclencheur ?
- Dans les SPA, naviguez entre les routes et vérifiez si le focus se déplace vers un titre significatif.
- Vérifiez la présence d’attributs
autofocuserrants et de scripts appelantfocus()sur des timers.
Quatrième étape : l’ordre de tabulation est-il sensé ?
- Vérifiez la présence de tabindex positifs (
tabindex="1", etc.). - Cherchez des éléments cachés mais tabbables.
- Confirmez que les contrôles désactivés ne sont pas encore focalisables (commun avec des composants personnalisés).
Cinquième étape : des modes spéciaux le cassent-ils ?
- Testez en mode couleurs forcées.
- Testez à un zoom 200 %.
- Testez en mode sombre si supporté.
Blague #2 : Un anneau de focus qui n’apparaît que sur l’écran de votre designer n’est pas une « expression de marque », c’est un angle mort de monitoring.
Tâches pratiques : commandes, sorties, décisions
Celles-ci sont destinées à être exécutées par quelqu’un qui a un dépôt, un build et un sens de l’urgence. Chaque tâche inclut une commande, ce que la sortie typique signifie,
et la décision que vous en tirez.
Tâche 1 : Trouver la suppression globale de l’outline dans le CSS
cr0x@server:~$ rg -n "outline\s*:\s*none" web/ styles/
styles/reset.css:42:*:focus{outline:none}
web/components/button.css:18:.btn:focus{outline:none}
Ce que la sortie signifie : Vous avez au moins deux règles qui suppriment les contours, une globale. La règle globale est le pyromane habituel.
Décision : Remplacez la suppression globale par une stratégie :focus-visible et assurez-vous que chaque élément interactif a un indicateur de focus visible.
Tâche 2 : Trouver la couverture :focus-visible (ou son absence)
cr0x@server:~$ rg -n ":focus-visible" web/ styles/
styles/focus.css:12::where(a, button, input):focus-visible { outline: 3px solid var(--ring); }
Ce que la sortie signifie : Vous avez une règle focus-visible. Bon début, mais vérifiez si elle s’applique à tous les composants et n’est pas écrasée.
Décision : Assurez-vous que le sélecteur couvre tous les contrôles interactifs pertinents et garde une faible spécificité afin que les styles de composants puissent l’étendre plutôt que de s’y opposer.
Tâche 3 : Identifier les éléments utilisant un tabindex positif
cr0x@server:~$ rg -n "tabindex\s*=\s*\"[1-9]" web/
web/pages/legacy-dashboard.html:88:
web/pages/legacy-dashboard.html:101:
Ce que la sortie signifie : Un tabindex positif est utilisé pour forcer un ordre de tabulation personnalisé. Cela crée souvent une navigation imprévisible entre navigateurs et lecteurs d’écran.
Décision : Refactorez pour l’ordre DOM avec tabindex="0" seulement quand nécessaire. Considérez le tabindex positif comme un bug sauf raison très spécifique et testée.
Tâche 4 : Localiser des div qui prétendent être des boutons
cr0x@server:~$ rg -n "role\s*=\s*\"button\"" web/
web/components/filters.html:14:
web/components/menu.html:55:
Ce que la sortie signifie : Vous avez des éléments interactifs personnalisés. Ils nécessitent des gestionnaires d’activation clavier, des styles de focus et une ARIA correcte.
Décision : Remplacez par des <button> quand c’est possible. Si ce n’est pas le cas, assurez-vous de l’activation par Enter/Space, du style focus-visible et des états ARIA corrects.
Tâche 5 : Vérifier les overflow qui coupent les anneaux de focus
cr0x@server:~$ rg -n "overflow:\s*hidden" web/components styles
web/components/card.css:7:.card{overflow:hidden;border-radius:12px}
web/components/modal.css:22:.modal-body{overflow:hidden}
Ce que la sortie signifie : Tout indicateur de focus implémenté avec box-shadow peut être tronqué à l’intérieur de ces conteneurs.
Décision : Préférez outline (non tronqué) ou ajustez la stratégie d’overflow du conteneur, ou ajoutez un anneau interne qui ne dépend pas d’ombres s’étendant à l’extérieur.
Tâche 6 : Détecter la présence du lien de saut à travers les templates
cr0x@server:~$ rg -n "Skip to main content" web/
web/layouts/base.html:3:Skip to main content
Ce que la sortie signifie : Le lien de saut existe dans la mise en page de base. Vérifiez maintenant qu’il n’est pas supprimé par des mises en page spécifiques et que #main existe partout.
Décision : Ajoutez un test CI pour échouer le build si #main est manquant dans le HTML rendu ou si le texte du lien de saut est absent.
Tâche 7 : Vérifier que la cible principale existe sur toutes les pages
cr0x@server:~$ rg -n 'id="main"' web/pages
web/pages/home.html:12:
web/pages/pricing.html:9:
web/pages/blog.html:15:
Ce que la sortie signifie : Une page utilise un <div id="main"> au lieu de <main> et peut ne pas être focalisable.
Décision : Standardisez sur <main id="main" tabindex="-1"> sur toutes les pages, ou assurez-vous que la cible peut recevoir le focus de façon fiable.
Tâche 8 : Exécuter une suite de tests d’accessibilité localement (Playwright)
cr0x@server:~$ npm test
> webapp@1.0.0 test
> playwright test
Running 18 tests using 4 workers
✓ a11y: skip link is reachable (1.2s)
✗ a11y: focus indicator visible on buttons (2.0s)
Error: expected focus ring to be visible on .btn-primary, but computed outline-style was 'none'
Ce que la sortie signifie : Le test a détecté que votre bouton principal n’a pas de contour visible lorsqu’il est focalisé.
Décision : Corrigez l’override CSS du composant et conservez le test. Ne le marquez pas instable. Un anneau de focus n’est pas une fonctionnalité optionnelle.
Tâche 9 : Identifier les conflits de spécificité affectant focus-visible
cr0x@server:~$ rg -n "\.btn.*:focus" web/components/button.css
18:.btn:focus{outline:none}
24:.btn-primary:focus-visible{outline:none;box-shadow:none}
Ce que la sortie signifie : Le CSS du composant supprime explicitement le style focus-visible. C’est la signature de « quelqu’un a voulu le faire disparaître ».
Décision : Supprimez ces règles, ou remplacez-les par un style focus-visible conforme. Si le design veut un style personnalisé, parfait—livrez un anneau visible, pas rien.
Tâche 10 : Vérifier l’usage errant d’autofocus
cr0x@server:~$ rg -n "\bautofocus\b" web/
web/pages/login.html:22:
web/components/modal.html:8:
Ce que la sortie signifie : Autofocus peut voler le focus de manière inattendue, surtout quand des modales se montent ou que des routes changent.
Décision : Conservez autofocus uniquement quand c’est clairement bénéfique (le champ de login est souvent acceptable). Remplacez l’autofocus en modale par une gestion explicite du focus à l’ouverture.
Tâche 11 : Valider que la cible du lien de saut est focalisable à l’exécution (vérification Node simple)
cr0x@server:~$ node -e "const fs=require('fs');const html=fs.readFileSync('dist/pricing.html','utf8');console.log(/id=\"main\"/.test(html), /tabindex=\"-1\"/.test(html));"
true true
Ce que la sortie signifie : Le HTML rendu contient id="main" et tabindex="-1".
Décision : Ajoutez ce type de vérification en CI pour les templates qui varient. C’est grossier, mais efficace pour attraper les régressions.
Tâche 12 : Inspecter le CSS buildé pour une suppression accidentelle des contours
cr0x@server:~$ rg -n "outline:none" dist/assets/app.css | head
1882:*:focus{outline:none}
45110:.btn:focus{outline:none}
Ce que la sortie signifie : Votre build inclut toujours une suppression globale des contours. Même si le code source semble correct, une dépendance ou une étape de build peut l’avoir réintroduit.
Décision : Corrigez à la source (reset stylesheet, override de dépendance ou pipeline de build). Puis ajoutez une vérification au build qui échoue si *:focus{outline:none} apparaît.
Tâche 13 : Lancer Lighthouse CI et interpréter l’échec lié au focus
cr0x@server:~$ npx lhci autorun
✅ .lighthouseci/ collected 1 run(s)
⚠️ Accessibility score: 0.92
- [fail] Background and foreground colors do not have a sufficient contrast ratio.
- [warn] Interactive elements do not have a focus indicator.
Ce que la sortie signifie : Les outils automatisés signalent des problèmes d’indicateur de focus. Ils ne pointeront peut-être pas le composant exact, mais c’est un signal que votre baseline n’est pas fiable.
Décision : Utilisez ceci comme garde-fou en CI. Puis complétez par des tests clavier ciblés sur les flux critiques (paiement, connexion, actions admin).
Tâche 14 : Utiliser Git pour repérer quand le focus s’est cassé
cr0x@server:~$ git log -p -S "outline:none" -- web/styles/reset.css | head -n 20
commit 7c2a1b9d3d2c1a4b9d0c1f8e3a2b7f3c1d9a8b7c
Author: dev
Date: Tue May 14 10:22:11 2024 -0700
Align focus styles with new design system
diff --git a/web/styles/reset.css b/web/styles/reset.css
@@ -39,6 +39,7 @@
* { box-sizing: border-box; }
-*:focus { outline: auto; }
+*:focus { outline: none; }
Ce que la sortie signifie : Un commit a intentionnellement supprimé les contours. Le message suggère un alignement design, mais le diff montre une régression d’accessibilité.
Décision : Revertir ou amender avec un style :focus-visible approprié. Ajoutez aussi des points de contrôle en revue pour que « alignement design » ne devienne pas une excuse universelle.
Trois mini-récits d’entreprise depuis le front
Mini-récit 1 : L’incident causé par une mauvaise hypothèse
Une entreprise SaaS de taille moyenne a déployé un nouvel en-tête de navigation : méga-menu, sélecteur de produit, notifications, le look habituel « on a grandi ».
L’équipe l’a livré derrière un feature flag, a fait un smoke test rapide, et a considéré la tâche terminée.
La mauvaise hypothèse : « Si ça marche à la souris, ça marche. » Ils avaient supprimé le contour par défaut globalement des années auparavant, et l’ancienne UI avait des styles de focus personnalisés
sur une poignée de composants. Le nouvel en-tête utilisait un dropdown tiers et un composant « pill » maison. Aucun des deux n’avait de style focus-visible.
Le premier signal n’est pas venu d’un audit d’accessibilité. Il est venu du support entreprise : la politique interne d’un client exigeait la navigation au clavier
pour certains flux, et le client ne pouvait pas réaliser une action admin critique sans perdre son repère dans l’en-tête.
L’équipe d’ingénierie l’a reproduit en quelques minutes : tabuler dans l’en-tête fonctionnait, mais rien n’était visiblement focalisé. Les gens cliquaient au hasard pour se remettre à flot.
L’UI « fonctionnait », mais elle était pratiquement inopérable au clavier seul. C’est une panne si le mode d’entrée de votre utilisateur est le clavier.
La correction a été brutalement simple : déployer un anneau :focus-visible de base sur tous les éléments interactifs, puis affiner composant par composant.
La leçon est restée : vous ne pouvez pas compter sur les bibliothèques de composants si vous sabotez le focus globalement.
Mini-récit 2 : L’optimisation qui s’est retournée contre eux
Une autre entreprise avait un mandat de performance. Leur bundle front-end était gonflé, ils ont introduit une purge CSS agressive et un sprint « modernize everything ».
La configuration de purge était réglée pour ne conserver que les sélecteurs détectés dans les templates au moment du build.
Le revers : les styles focus-visible étaient définis dans une feuille partagée et appliqués via des sélecteurs :where(), plus un petit ensemble de classes utilitaires qui
n’apparaissaient que dynamiquement (modales montées, découpage de code par route). La purge n’a pas « vu » ces sélecteurs lors de l’analyse statique. Elle les a retirés.
En production, les utilisateurs clavier ont commencé à signaler des comportements étranges : certaines pages avaient des anneaux, d’autres non. Certaines modales allaient bien, d’autres étaient invisibles.
Cela semblait aléatoire, ce qui est le pire type de bug car cela déclenche folklore et correctifs par cargo-cult.
Le débogage a pris plus de temps que nécessaire parce que l’équipe a d’abord accusé des bizarreries du navigateur. Le vrai problème était le pipeline de build qui supprimait du CSS critique.
Une fois qu’ils ont inspecté le CSS construit et comparé au source, c’était évident.
La correction : safelister les sélecteurs focus-visible et tout motif de classes utilisé pour les anneaux de focus. Puis ajouter une vérification automatisée pour la présence du CSS de focus de base dans
l’artéfact final. La performance compte, mais pas au prix de la capacité de vos utilisateurs à naviguer.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une grande équipe sur un outil admin interne avait une habitude peu glamour : chaque nouveau composant devait passer un test rapide au clavier avant le merge.
Ce n’était pas un grand processus formel. Juste une checklist dans le template de PR : tabuler, Shift+Tab revenir, activer avec Enter/Space, confirmer l’anneau de focus visible,
et vérifier que l’ordre de tabulation a du sens.
Lors d’un refactor, ils ont remplacé un select natif par un « searchable dropdown » personnalisé. C’était joli. Cela a aussi introduit un piège de focus subtil : quand le dropdown
s’ouvrait, le focus allait dans une listbox, mais à la fermeture, le focus ne revenait pas au déclencheur. Les utilisateurs clavier se retrouvaient « derrière » l’UI, tabulant à travers des éléments cachés.
Le développeur l’a détecté avant la revue parce que la checklist l’a obligé à essayer le flux avec Tab. Ils l’ont corrigé en stockant explicitement l’élément déclencheur, en déplaçant le focus
dans le dropdown à l’ouverture, et en restaurant le focus à la fermeture.
Pas d’incident. Pas de tickets enragés. Pas d’escalade exécutive. Juste une petite pratique ennuyeuse empêchant une défaillance lente d’être livrée.
En exploitation, nous vénérons les systèmes ennuyeux parce qu’ils sont prévisibles. L’accessibilité clavier est la même chose. Rendez-la ennuyeuse. Rendez-la standard.
Erreurs courantes : symptômes → cause racine → solution
1) Symptom : « Tab fonctionne, mais je ne vois pas où je suis. »
Cause racine : Suppression globale des contours (*:focus{outline:none}) sans remplacement visible, ou couleur de l’anneau trop proche de l’arrière-plan.
Solution : Implémentez un anneau de base :focus-visible avec contraste et offset suffisants ; retirez la suppression blanket ou scopez-la soigneusement.
2) Symptom : « L’anneau de focus apparaît au clic et les designers détestent ça. »
Cause racine : Styliser :focus au lieu de :focus-visible, ou des navigateurs traitant l’interaction comme clavier à cause des heuristiques de modalité.
Solution : Déplacez l’anneau visible sur :focus-visible. Gardez un style minimal sur :focus seulement si nécessaire pour le fallback.
3) Symptom : « Certains boutons montrent le focus, d’autres non. »
Cause racine : Overrides au niveau composant qui suppriment outline/box-shadow ; conflits de spécificité CSS ; purge CSS supprimant les sélecteurs de base.
Solution : Auditez le CSS des composants pour outline:none ; gardez la règle de focus de base à faible spécificité ; safelistez les sélecteurs dans la config de purge ; ajoutez des vérifications au build.
4) Symptom : « L’anneau de focus est coupé. »
Cause racine : Anneaux basés sur box-shadow tronqués par overflow:hidden ou par des conteneurs de scroll.
Solution : Utilisez outline pour l’anneau principal ; augmentez outline-offset ; changez la stratégie d’overflow du conteneur ou ajoutez du padding pour éviter la coupe.
5) Symptom : « Le lien de saut existe mais ne fait rien. »
Cause racine : Ancre cible manquante, ID dupliqués, ou élément cible non focalisable dans certains navigateurs.
Solution : Assurez-vous que id="main" existe une seule fois ; ajoutez tabindex="-1" à la cible ; confirmez sa présence dans tous les templates.
6) Symptom : « Les utilisateurs clavier restent coincés dans une modale. »
Cause racine : Implémentation du focus trap cassée, ou ordre de tabulation incluant des éléments cachés hors de la modale.
Solution : Implémentez un focus trap testé ; désactivez le scroll et l’interaction en arrière-plan ; marquez l’arrière-plan comme inert si supporté ; restaurez le focus à la fermeture.
7) Symptom : « Après changement de route, le focus est perdu ou reste sur l’ancienne UI. »
Cause racine : La navigation SPA change le contenu sans déplacer le focus ; le focus reste sur un élément supprimé ou sur un élément de nav.
Solution : Lors du changement de route, déplacez le focus vers un titre significatif ou le conteneur principal (avec tabindex="-1"). Gardez un comportement cohérent entre les routes.
8) Symptom : « Le lecteur d’écran annonce des choses bizarres ; le comportement clavier est incohérent. »
Cause racine : Éléments interactifs personnalisés sans sémantique ; rôles/états ARIA incorrects ; mélange de role="button" avec des liens imbriqués, etc.
Solution : Préférez les contrôles natifs. Si personnalisés, implémentez les événements clavier appropriés, les états ARIA et assurez-vous que le style de focus s’applique au nœud réellement focalisable.
Listes de contrôle / plan étape par étape
Étape par étape : livrer une base fiable en un sprint
- Inventaire de la suppression du focus. Cherchez
outline:noneetbox-shadow:nonesur les états de focus. Supprimez ou justifiez chaque occurrence. - Ajoutez une règle de base
:focus-visible. Couvrez les ancres, boutons, champs de formulaire et tout élément avec tabindex. - Définissez des tokens d’anneau. Choisissez couleur(s), largeur et offset de l’anneau comme variables du système de design. Faites-les compatibles thème.
- Ajoutez un lien de saut dans la mise en page de base. Assurez-vous que
#mainexiste sur toutes les pages et est focalisable avectabindex="-1". - Corrigez les 10 composants principaux. Boutons, liens, inputs, selects, menus, onglets, puces, toggles, modales et dropdowns.
- Testez les flux critiques uniquement au clavier. Connexion, paiement, modifications de paramètres, actions destructrices, et tout ce qui peut bloquer un utilisateur.
- Couvrez les couleurs forcées et le mode sombre. Ajoutez
@media (forced-colors: active)et vérifiez la visibilité de l’anneau dans le thème sombre. - Ajoutez des vérifications CI. Faites échouer les builds si le CSS de focus de base est absent de l’artéfact ou si le lien de saut / cible main est absent.
Checklist d’acceptation navigation clavier (prête pour PR)
- L’ordre de tabulation suit l’ordre visuel (ou au moins ne surprend pas).
- Pas de valeurs tabindex positives sans raison écrite et tests.
- Chaque élément interactif a un indicateur de focus visible lors de la navigation au clavier.
- Le lien de saut est le premier élément atteignable et fonctionne.
- Les modales piègent le focus et restaurent le focus sur le déclencheur à la fermeture.
- Les dropdowns et menus supportent Échap pour fermer et rendre le focus.
- L’anneau de focus n’est pas coupé par les styles des conteneurs.
- Le mode couleurs forcées montre toujours un focus clair.
Checklist du système de design : « joli » sans casser les utilisateurs
- Largeur d’anneau ≥ 2px dans la plupart des contextes ; 3px est plus sûr.
- Offset de l’anneau pour le rendre distinct des bordures.
- Contraste de la couleur de l’anneau testé sur toutes les surfaces (cartes, bannières, inputs, états « désactivé »).
- Ne comptez pas seulement sur un changement de couleur interne à l’élément (comme changer le fond de 5%).
- Privilégiez
outline(ou incluez outline) pour la résilience en couleurs forcées.
FAQ
1) Dois-je utiliser outline: none un jour ?
Oui, mais uniquement avec un remplacement visible pour la navigation au clavier. Le pattern sûr est : supprimer l’outline par défaut sur :focus, ajouter un style fort sur :focus-visible.
Si vous ne pouvez pas garantir le remplacement à travers les composants, ne le supprimez pas globalement.
2) Pourquoi :focus-visible s’affiche parfois au clic de souris ?
Les navigateurs utilisent des heuristiques. Si vous avez récemment utilisé le clavier, ou si vous interagissez avec un contrôle où l’indication de focus aide (comme les champs texte),
le navigateur peut décider que le focus est « visible ». Ne combattez pas cela trop fortement. L’objectif est l’utilisabilité, pas la pureté esthétique.
3) Un changement subtil de couleur de fond suffit-il comme indicateur de focus ?
Généralement non. Les remplissages subtils échouent sur des arrière-plans chargés, en mode sombre et sur des écrans de faible qualité. Utilisez un anneau clairement visible et qui survit aux couleurs forcées.
Pensez « évident », pas « de bon goût ».
4) Pourquoi mon anneau de box-shadow disparaît dans certains composants ?
Parce que quelque chose dans la mise en page le coupe : overflow:hidden, conteneurs de scroll, ou contextes de stacking. Utilisez outline comme anneau principal,
ou assurez-vous d’avoir assez d’espace et aucun clipping autour des éléments focalisés.
5) Les liens de saut comptent-ils dans les applications mono-page (SPA) ?
Oui. Les SPA ont souvent des en-têtes persistants et des zones de contenu dynamiques. Un lien de saut plus une gestion du focus cohérente lors des changements de route rendent l’app stable pour les utilisateurs clavier.
6) Où doit aller le focus après un changement de route ?
Généralement vers le titre principal (comme le <h1>) ou le conteneur principal. Rendez la cible focalisable avec tabindex="-1" et déplacez le focus intentionnellement.
Ne laissez pas le focus sur l’élément de nav qui a déclenché le changement ; c’est ainsi que les utilisateurs perdent le contexte.
7) Quel est le problème avec tabindex="5" et consorts ?
Les tabindex positifs créent un ordre de tabulation séparé qui peut devenir incohérent et fragile quand le DOM change. Ils cassent aussi les attentes des utilisateurs d’assistance.
Préférez l’ordre DOM et tabindex="0" seulement quand il faut rendre un élément non natif focalisable.
8) Comment rendre le style de focus cohérent dans un système de design ?
Définissez des tokens d’anneau (couleur, largeur, offset) à la racine, appliquez une règle de base à faible spécificité, puis permettez aux composants d’étendre plutôt que d’écraser.
Ajoutez des vérifications CI qui s’assurent que les règles :focus-visible existent dans les artefacts finaux.
9) Ai-je besoin d’un anneau de focus sur chaque élément ?
Uniquement sur les éléments qui peuvent recevoir le focus : contrôles interactifs et tout élément que vous avez rendu focalisable (liens, boutons, inputs, widgets personnalisés avec tabindex).
Ne rendez pas des conteneurs aléatoires focalisables juste pour « matcher le comportement hover ». Cela crée du bruit et de la fatigue de tabulation.
10) Et si le design veut un style de focus personnalisé par composant ?
Très bien. La contrainte est la visibilité et la cohérence. Gardez un anneau de base comme fallback, puis améliorez. Dès que les styles personnalisés commencent à supprimer l’anneau,
vous retournez en zone de panne.
Conclusion : prochaines étapes à livrer cette semaine
Les états de focus accessibles ne sont pas un « bonus ». Ce sont des infrastructures d’interaction essentielles. Traitez-les comme vous traitez TLS : basez-les, faites-les respecter et n’autorisez pas des composants au hasard
à se désinscrire parce que quelqu’un n’aime pas leur apparence dans une capture d’écran.
Prochaines étapes pratiques :
- Implémentez un anneau de base
:focus-visibleutilisantoutlineavec offset, plus un halo optionnel pour l’esthétique. - Ajoutez un lien de saut dans la mise en page de base et standardisez
<main id="main" tabindex="-1">sur toutes les pages. - Retirez la suppression globale du focus sauf si vous la remplacez correctement.
- Corrigez les composants qui outrepensent
:focus-visibleet ajoutez des tests pour les garder honnêtes. - Exécutez le mode d’intervention rapide chaque fois que quelqu’un dit « le clavier est bizarre ». Ce n’est généralement pas bizarre ; c’est cassé.