Sur le site d’un vendeur d’accessoires pour vélos électriques, la liste de produits se recalcule en Ajax dès qu’on change le tri ou qu’on coche un filtre de marque. Au clavier, le résultat était désagréable : chaque clic renvoyait le focus tout en haut du document, obligeant à retraverser tout le menu pour enchaîner deux filtres. À la souris, personne ne le remarquait. Au clavier, c’était un parcours du combattant.
Le coupable est presque toujours le même : la zone de résultats est remplacée en bloc par du nouveau HTML (innerHTML ou un composant qui se démonte puis se remonte), et le nœud qui portait le focus disparaît avec l’ancien contenu. Le navigateur, ne sachant plus où poser le focus, le renvoie sur <body>. Ce guide détaille, étape par étape, comment garder la main sur le focus pendant un tri ou un filtrage, sans toucher à la pagination, qui suit une logique différente.
Pourquoi le focus saute au mauvais endroit
Quand un composant de tri déclenche une requête Ajax, la séquence typique est : capture du clic, appel réseau, puis remplacement du conteneur de résultats par le HTML retourné. Si le bouton ou le lien de tri qui avait le focus se trouve dans ce conteneur remplacé, il est détruit avant que le navigateur ait pu décider où aller ensuite. Résultat : le focus atterrit sur document.body, ce qui, pour une personne au clavier, ressemble à une téléportation en haut de la page.
Étape 1 : sortir les commandes de tri du conteneur qui se recharge
La première correction, souvent suffisante à elle seule, consiste à séparer la barre de commandes (tri, filtres) de la zone qui est réellement remplacée par la réponse Ajax. Si le <select> de tri vit en dehors du <div id="resultats"> qui reçoit le nouveau HTML, il ne disparaît jamais et garde naturellement le focus.

Étape 2 : mémoriser l’élément actif avant la mise à jour
Quand la séparation n’est pas possible (par exemple un filtre à cases à cocher situé dans le panneau qui se régénère), il faut mémoriser explicitement l’élément qui a le focus avant de lancer le rendu, puis le restaurer après. On ne peut pas se contenter d’un index de position dans la liste, car l’ordre des filtres change souvent après un tri. Un identifiant stable est indispensable :
function surRafraichissement(nouveauHTML) {
const actif = document.activeElement;
const idActif = actif && actif.dataset.filtreId;
conteneur.innerHTML = nouveauHTML;
if (idActif) {
const cible = conteneur.querySelector(`[data-filtre-id="${idActif}"]`);
if (cible) {
cible.focus();
} else {
// La case a disparu (ex. marque exclue par le nouveau tri) :
// on retombe sur le premier champ du panneau plutôt que sur body.
conteneur.querySelector('input, select, button')?.focus();
}
}
}
Le point important est le repli : si l’élément identifié n’existe plus après le nouveau rendu, il faut prévoir une cible de secours plutôt que de laisser le navigateur choisir à notre place.
Étape 3 : annoncer le résultat sans voler le focus
Une fois le focus stabilisé, il reste un problème : comment une personne utilisant un lecteur d’écran sait-elle que la liste a changé, si son focus reste sur la case à cocher qu’elle vient d’actionner ? La réponse n’est pas de déplacer le focus sur le titre des résultats — ce serait aussi désorientant que la perte de focus initiale — mais d’ajouter une région aria-live="polite" qui annonce le nombre de résultats sans rien déplacer :
- Une zone dédiée, visuellement discrète, avec
aria-live="polite"etaria-atomic="true". - Un texte mis à jour à chaque rafraîchissement : « 14 produits affichés, triés par prix croissant ».
- Aucun appel à
.focus()sur cette zone : elle est seulement lue, jamais ciblée.
Étape 4 : tester la séquence complète au clavier
Le test qui révèle vraiment le problème n’est pas « le tri fonctionne-t-il », mais « puis-je enchaîner trois filtres sans revenir en haut de page ». Concrètement :
- Ouvrir la page au clavier uniquement (souris débranchée si besoin, pour se forcer).
Tabjusqu’au premier filtre, l’activer avecEspaceouEntrée.- Vérifier que le focus visible reste sur ce même filtre après le rechargement.
- Répéter avec un deuxième puis un troisième filtre, sans jamais repartir de
<body>. - Activer NVDA ou VoiceOver et vérifier que l’annonce du nombre de résultats est bien lue, sans répétition ni silence.
Sur ce type de composant, je préfère toujours tester avec un filtre qui fait disparaître l’élément actif du DOM (une marque qui n’a plus de résultat, par exemple). C’est le scénario que les développeurs oublient le plus souvent, et c’est précisément celui qui casse le focus en production.
En résumé
Un composant de tri ou de filtre accessible au clavier ne demande pas une refonte lourde : il suffit de savoir où va le focus avant de remplacer le DOM, de le restaurer sur un identifiant stable plutôt que sur une position, et de prévoir une cible de repli. Ajoutez une annonce aria-live pour le contenu, sans jamais y déplacer le focus, et le composant devient utilisable aussi bien à la souris qu’au clavier ou au lecteur d’écran. La pagination, elle, pose d’autres questions — notamment le changement d’URL — qui méritent un traitement séparé.