English

Posez la souris : tout sur la navigation clavier

Table des matières

Cinq secondes pour comprendre

Fais le test. Là, maintenant. Pose ta souris, ouvre un nouvel onglet sur ton site, et appuie sur Tab.

Peut-être que tout fonctionne, le focus est visible, l'ordre est logique, chaque élément interactif est atteignable. Si c'est le cas, bravo, c'est loin d'être la norme. Mais peut-être que tu ne vois plus où tu es, le focus a disparu quelque part dans le CSS. Ou que tu navigues d'un élément à l'autre dans un ordre qui n'a aucun rapport avec ce que tu lis à l'écran. Ou que certains boutons ou liens sont tout simplement inatteignables.

Quel que soit le résultat, tu viens de vivre en 5 secondes ce que vivent des millions de personnes à chaque visite. La différence, c'est qu'elles n'ont pas le choix de reprendre la souris.

Cet article va te montrer qui sont ces personnes, pourquoi tant de sites les ignorent, et comment construire une navigation clavier qui ne les laisse pas dans le noir.

Pour qui le clavier n'est pas un choix

Michael J. Fox (Marty McFly dans Retour vers le futur) ne peut plus conduire depuis des années. Les tremblements causés par la maladie de Parkinson rendent aussi l'utilisation d'une souris extrêmement difficile. Cliquer précisément sur un bouton, maintenir le curseur sur un élément, double-cliquer, des gestes anodins qui deviennent des obstacles réels quand tes mains ne répondent plus comme tu le souhaites.

Pour ces personnes, la navigation au clavier n'est pas une préférence. C'est la seule option.

Et Michael J. Fox est loin d'être seul. La maladie de Parkinson touche plus de 10 millions de personnes dans le monde. Mais ce n'est qu'une condition parmi d'autres. Les troubles musculo-squelettiques, les séquelles d'AVC, les paralysies partielles, les amputations touchent des millions de personnes supplémentaires qui dépendent elles aussi d'une navigation clavier fonctionnelle.

Et puis il y a les situations temporaires. Un bras cassé. Une entorse au poignet. Une tendinite après une semaine de code intensive. Un bébé endormi dans un bras pendant que l'autre navigue. Le handicap n'est pas toujours permanent, mais l'inaccessibilité, elle, est constante.

Si ton interface ne fonctionne pas au clavier, tu exclus ces personnes. Silencieusement, sans le savoir. Et comme pour les couleurs qui mentent, 56% des utilisateurs mécontents ne disent rien, ils partent.

Le focus invisible

Il y a une ligne de CSS que tu as probablement déjà vue, ou écrite :

reset.css
*:focus {
  outline: none;
}

Cette ligne supprime le seul indicateur visuel qu'un utilisateur clavier a pour savoir où il se trouve sur la page. C'est l'équivalent de retirer les numéros de rue d'un quartier entier : techniquement, les maisons sont toujours là, mais personne ne peut les trouver.

Pourquoi tant de projets l'incluent ? Parce que l'outline par défaut du navigateur, ce rectangle bleu ou pointillé, apparaît aussi au clic de souris. Les designers le trouvent inesthétique, les devs le suppriment, et personne ne remarque le problème parce que l'équipe utilise une souris.

La solution existe depuis des années et tient en un mot : :focus-visible. Ce pseudo-sélecteur n'applique le style de focus que lorsque le navigateur estime que l'utilisateur bénéficierait d'un indicateur visible, c'est-à-dire lors de la navigation clavier, pas au clic souris.

focus.css
/* Supprime l'outline par défaut uniquement
   quand focus-visible ne s'applique pas */
:focus:not(:focus-visible) {
  outline: none;
}
 
/* Le vrai style de focus — visible, robuste, adaptatif */
:focus-visible {
  outline: var(--outline-width) solid var(--color-primary);
  outline-offset: var(--outline-offset);
  box-shadow:
    0 0 0 var(--outline-offset) var(--color-background),
    0 0 0 var(--outline-spread) var(--color-primary);
}
 
@media (forced-colors: active) {
  :focus-visible {
    outline: var(--outline-width) solid Highlight;
    outline-offset: var(--outline-offset);
    box-shadow: none;
  }
}
 
@media (prefers-reduced-motion: no-preference) {
  :focus-visible {
    transition: outline-offset var(--duration-fast) ease;
  }
}

Ce pattern, le double ring, crée un contour visible sur tous les fonds. La première ombre en couleur de fond isole l'outline, la seconde en couleur primaire le rend visible quel que soit l'arrière-plan. Exprime les épaisseurs (les custom properties --outline-width et compagnie) en em ou rem pour que le focus s'adapte à la taille de fonte de l'élément, et donc au zoom. Et le bloc forced-colors assure que le focus reste visible quand l'utilisateur impose son propre thème. La box-shadow disparaît en mode contraste élevé, d'où le outline seul avec le mot-clé système Highlight.

Le critère 2.4.13 (Focus Appearance), nouveau dans les WCAG 2.2, exige que l'indicateur couvre au moins la surface d'un périmètre de 2 px autour du composant, avec un ratio de contraste de 3:1 entre l'état focus et l'état non-focus. Le double ring qu'on vient de voir dépasse ces exigences.

L'ordre qui ment

Le focus suit le DOM, pas le CSS. C'est une phrase simple, mais ses conséquences sont partout.

Quand tu utilises flexbox avec order, row-reverse ou column-reverse, tu réorganises ce que l'utilisateur voit, mais pas ce que le clavier parcourt. grid peut produire le même effet quand grid-area ou grid-template-areas placent les éléments dans un ordre visuel différent du DOM, mais ce n'est pas systématique. L'utilisateur à la souris voit une interface logique. L'utilisateur au clavier peut naviguer dans un ordre qui ne correspond à rien de ce qu'il voit à l'écran.

order-mismatch.html
<nav class="nav-main">
  <a href="/about">À propos</a>
  <a href="/blog">Blog</a>
  <a href="/contact">Contact</a>
</nav>
order-mismatch.css
.nav-main {
  display: flex;
}
 
.nav-main a:nth-child(1) {
  order: 3;
}
 
.nav-main a:nth-child(2) {
  order: 1;
}
 
.nav-main a:nth-child(3) {
  order: 2;
}

Visuellement : Blog → Contact → À propos. Au clavier : À propos → Blog → Contact. Le focus saute dans tous les sens, et l'utilisateur ne comprend pas pourquoi.

La solution n'est pas tabindex. Du moins, pas avec des valeurs positives. tabindex="1", tabindex="2", tabindex="3", ça semble logique, mais ça crée un système parallèle de priorités qui entre en conflit avec l'ordre naturel du DOM. Chaque élément avec un tabindex positif passe avant tous les autres éléments de la page. Ajoute un nouveau composant et tout l'ordre est à revoir.

tabindex-a-eviter.html
<!-- Ne fais pas ça -->
<button tabindex="3">Envoyer</button>
<button tabindex="1">Annuler</button>
<button tabindex="2">Brouillon</button>

La vraie solution est plus simple : fais correspondre l'ordre du HTML à l'ordre visuel. Si le design demande un ordre d'affichage différent, change le HTML, pas le CSS order. Et si tu dois rendre focalisable un élément qui ne l'est pas nativement, la première question à te poser est : pourquoi cet élément n'est-il pas un <button> ou un <a> ? Les éléments HTML interactifs natifs sont focalisables par défaut, annoncés correctement par les lecteurs d'écran, et activables au clavier sans effort supplémentaire. tabindex="0" existe, mais c'est un dernier recours, pas un premier réflexe.

Comment vérifier que l'ordre est cohérent ? Accessibility Insights for Web propose une fonctionnalité "Tab Stops" qui affiche l'ordre exact du focus sur ta page, numéroté, superposé directement sur le contenu. En quelques secondes, tu vois si les numéros suivent la logique visuelle ou sautent dans tous les sens.

Les pièges à clavier

Il y a pire que le focus invisible ou l'ordre incohérent. Le focus qui entre quelque part et ne peut plus en sortir.

Ça s'appelle un keyboard trap, un piège à clavier. Tu navigues au Tab, tu arrives dans un composant, et quoi que tu fasses, Tab te ramène au même endroit. Échap ne fait rien. Le seul moyen de s'en sortir : recharger la page.

Les coupables habituels : les modales mal implémentées, les lecteurs vidéo custom, les éditeurs de texte riche, les widgets de date, les menus déroulants qui capturent le focus sans le relâcher.

Le paradoxe, c'est que dans certains cas, confiner le focus est exactement le bon comportement. Quand une modale s'ouvre, le focus doit rester à l'intérieur, sinon l'utilisateur clavier peut interagir avec le contenu derrière la modale, ce qui est à la fois confus et potentiellement dangereux (valider un formulaire qu'il ne voit pas, par exemple).

La différence entre un focus trap (voulu) et un keyboard trap (bug) tient en trois règles :

  1. L'utilisateur peut sortir : Échap ferme la modale et renvoie le focus là où il était avant l'ouverture
  2. Le focus est annoncé : le lecteur d'écran sait qu'une modale est ouverte et connaît son contenu
  3. Le reste de la page est désactivé : pas juste visuellement, mais pour le clavier et les lecteurs d'écran aussi

C'est ce troisième point qui posait historiquement problème. Désactiver le reste de la page demandait d'ajouter aria-hidden="true" et tabindex="-1" sur tous les éléments en dehors de la modale. Fragile, verbeux, source de bugs.

L'élément <dialog> natif, ouvert avec .showModal(), résout tout ça sans librairie externe :

dialog.html
<button id="open-confirm" type="button">Supprimer</button>
 
<dialog id="confirm">
  <form method="dialog">
    <h2>Confirmer la suppression</h2>
    <p>Cette action est irréversible.</p>
    <button type="submit" value="cancel" autofocus>Annuler</button>
    <button type="submit" value="confirm">Confirmer la suppression</button>
  </form>
</dialog>
dialog.js
const dialog = document.getElementById('confirm');
 
document.getElementById('open-confirm').addEventListener('click', () => {
  dialog.showModal();
});
 
dialog.addEventListener('close', () => {
  if (dialog.returnValue === 'confirm') {
    // action de suppression
  }
});
 
dialog.addEventListener('click', (e) => {
  if (e.target === dialog) dialog.close();
});

Un détail : un clic dans le padding du <dialog> lui-même a aussi e.target === dialog et fermerait la modale. Pour que seul le backdrop déclenche la fermeture, enveloppe le contenu dans un conteneur qui occupe toute la surface de la modale (padding: 0 sur le <dialog>, le padding sur le conteneur).

showModal() fait le travail : le focus est confiné automatiquement dans la modale, le reste de la page devient inerte, et Échap ferme la modale. Le focus devrait revenir à l'élément déclencheur à la fermeture, mais le comportement varie selon les navigateurs. En pratique, il est plus sûr de gérer ce retour explicitement en JavaScript. Pas de aria-hidden manuel, pas de tabindex="-1" sur chaque élément extérieur, pas de scroll lock en JavaScript.

dialog.css
dialog::backdrop {
  background-color: light-dark(
    hsl(0 0% 0% / var(--backdrop-opacity)),
    hsl(0 0% 0% / var(--backdrop-opacity-dark))
  );
}

Pas de z-index à gérer, pas de scroll lock en JavaScript, pas de aria-modal. Le navigateur fait le travail.

Construire une navigation clavier solide

Les sections précédentes t'ont montré les problèmes les plus courants : focus invisible, ordre incohérent, pièges à clavier. Voici les fondations pour ne pas les reproduire.

Le lien d'évitement

Sur la plupart des sites, le premier Tab amène le focus sur le logo, le deuxième sur le premier lien de navigation, le troisième sur le deuxième lien, et ainsi de suite. Un utilisateur clavier doit traverser toute la navigation avant d'atteindre le contenu, à chaque page.

Le lien d'évitement (ou skip link) résout ça : un lien invisible au repos, qui apparaît au premier Tab et permet de sauter directement au contenu principal.

skip-link.html
<body>
  <a href="#main-content" class="skip-link">
    Aller au contenu principal
  </a>
  <header><!-- liens de navigation --></header>
  <main id="main-content">
    <!-- contenu -->
  </main>
</body>
skip-link.css
.skip-link {
  position: absolute;
  inset-inline-start: -100vw;
  background: var(--color-primary);
  color: var(--color-primary-foreground);
  padding: var(--space-2) var(--space-4);
  text-decoration: none;
  z-index: 1000;
}
 
.skip-link:focus-visible {
  inset-inline-start: var(--space-2);
  inset-block-start: var(--space-2);
}

Pas de display: none, ça retirerait le lien de l'arbre d'accessibilité. On le positionne hors écran et on le ramène au focus-visible. Propriétés logiques (inset-inline-start) pour la compatibilité RTL.

Les 5 touches à toujours supporter

Tout composant interactif doit répondre à un minimum de 5 touches :

  • Tab : passer au prochain élément focalisable
  • Maj + Tab : revenir à l'élément précédent
  • Entrée : activer l'élément (lien, bouton)
  • Espace : activer aussi les boutons, cases à cocher et interrupteurs. Un bouton custom qui ne répond qu'à Entrée est cassé
  • Échap : fermer, annuler, quitter le contexte courant

Les éléments HTML natifs (<a>, <button>, <input>, <select>) gèrent Tab, Entrée et Espace par défaut. Échap, en revanche, n'est natif que dans quelques contextes (un <dialog> ouvert avec showModal(), un <select> déplié) : dès que ton composant ouvre un contexte, c'est ton JavaScript qui doit l'écouter. C'est une raison de plus d'utiliser le HTML sémantique plutôt que des <div> avec des gestionnaires d'événements manuels.

Le focus après un changement de route

Dans une SPA, changer de page ne recharge pas le document, le focus reste là où il était. L'utilisateur clavier ne sait pas que le contenu a changé. Le lecteur d'écran non plus.

Deux approches :

  • Envoyer le focus sur le <h1> de la nouvelle page : l'utilisateur sait immédiatement où il est
  • Envoyer le focus sur le conteneur principal (<main>) : l'utilisateur peut commencer à naviguer le contenu

Dans les deux cas, l'élément cible doit avoir tabindex="-1" pour être focalisable programmatiquement sans entrer dans l'ordre de tabulation naturel.

Les critères d'ensemble

Le RGAA et les WCAG encadrent la navigation clavier à travers plusieurs critères :

  • RGAA 10.7 — le focus doit être visible
  • RGAA 12.6 — les zones de regroupement peuvent être atteintes ou évitées
  • RGAA 12.7 — un lien d'évitement à la zone de contenu principal est présent
  • RGAA 12.8 — l'ordre de tabulation est cohérent
  • RGAA 12.9 — la navigation ne doit pas contenir de piège au clavier
  • WCAG 2.4.13 — apparence minimale du focus (niveau AAA, non couvert par le RGAA)

Tous ces critères convergent vers la même idée : l'utilisateur clavier doit savoir où il est, savoir où il va, et pouvoir en sortir.

Et pour vérifier tout ça en une passe, le test le plus efficace reste le plus simple : pose ta souris, et navigue ton site au Tab. Si tu peux accomplir chaque action, dans un ordre logique, sans perdre le focus et sans rester coincé, ton site respecte l'essentiel.

Sources