Tiroir de navigation mobile pour la documentation qui ne casse pas : superposition, verrouillage du défilement et gestion du focus

Cet article vous a aidé ?

Les sites de documentation adorent le tiroir de navigation qui glisse. Les utilisateurs aussi — jusqu’à ce qu’il les piège sur une page, fasse ramer leur iPhone jusqu’à l’oubli ou rende la navigation au clavier aussi agréable qu’une spéléologie sans lampe frontale.

Si vous gérez des systèmes en production (ou si vous êtes la personne qu’on réveille quand « la doc est inutilisable sur mobile »), vous ne voulez pas d’un tiroir astucieux. Vous voulez un tiroir ennuyeux et prévisible : superposition correcte, verrouillage du défilement fiable et gestion du focus qui ne régresse pas à chaque mise à jour d’un framework.

À quoi ressemble un « bon » tiroir de documentation

Un tiroir de navigation mobile pour la documentation a trois missions, et la plupart des implémentations en ratent au moins une :

  1. Afficher la navigation sans perdre le contexte : barre latérale qui glisse, hiérarchie claire, fermeture rapide.
  2. Empêcher la page en arrière-plan d’interférer : superposition réelle, verrouillage réel du défilement, pas de clics fantômes.
  3. Respecter les entrées humaines : tactile, clavier, lecteurs d’écran, préférences de mouvement réduit et cas limites de navigateurs bizarres.

« Bon » signifie que les comportements suivants sont vrais sur les pires appareils et navigateurs (bonjour iOS Safari) :

  • Ouverture du tiroir : le reste de la page est inerte (pas de défilement, pas de clic, pas de focus).
  • Fermeture du tiroir : la page retourne exactement à la position de défilement précédente et le focus revient sur le bouton qui l’a ouvert.
  • Clavier : Tab reste à l’intérieur du tiroir ; Échap le ferme ; le bouton d’ouverture est détectable.
  • Tactile : l’arrière-plan ne « rebondit » pas sous la superposition ; pas de sélection de texte accidentelle.
  • Routage : la navigation ferme systématiquement le tiroir et ne laisse pas le document dans un état verrouillé.
  • Performance : les animations n’entraînent pas de thrash de layout ; la superposition n’est pas un réchauffeur GPU.

Opinion : traitez le tiroir comme une modale. Pas visuellement, mais comportementalement. Si vous n’autoriseriez pas le fond à défiler derrière une boîte de dialogue modale, ne l’autorisez pas derrière le tiroir de navigation non plus. Les pages de doc sont longues, donc les modes de panne sont plus bruyants.

Faits et historique qui expliquent les problèmes actuels

Comprendre comment on en est arrivé là rend les bugs actuels moins mystérieux et plus faciles à corriger de façon reproductible.

  1. L’icône « hamburger » n’a pas été inventée pour les téléphones. Elle est apparue dans des stations de travail des années 1980 ; le mobile l’a juste rendue célèbre.
  2. Mobile Safari a longtemps résisté à un vrai verrouillage du défilement parce que le défilement passait par une voie de compositeur spécialisée ; les développeurs web ont essayé de lutter avec des astuces.
  3. Les éléments en position fixe sur iOS se réalignaient lors du masquage/affichage de la barre d’adresse. Certains de ces cas limites provoquent encore du jitter.
  4. « 100vh » mentait historiquement sur mobile parce que l’UI du navigateur prenait de l’espace dynamiquement ; les tiroirs qui supposent une hauteur de viewport stable se font couper ou débordent bizarrement.
  5. Les contextes d’empilement sont devenus plus capricieux au fur et à mesure des fonctionnalités CSS. Des propriétés comme transform, filter et opacity créent de nouveaux contextes d’empilement ; des superpositions se retrouvent mystérieusement sous les en-têtes.
  6. Le web n’avait pas de primitive « inert » pendant des années, donc les équipes la simulaient en désactivant pointer-events ou en interceptant le focus. L’attribut inert est maintenant largement utilisable — mais nécessite toujours des tests.
  7. L’hydratation des frameworks a changé le profil des pannes. Du HTML rendu côté serveur qui devient interactif plus tard peut brièvement permettre le défilement ou le focus du fond avant que le JS n’attache les handlers.
  8. Le flou de l’arrière-plan est devenu populaire car c’est premium, puis tout le monde a découvert que ça ressemble aussi à des frames manquantes sur des appareils milieu de gamme.

Une citation à garder au mur, parce que les tiroirs paraissent « UI » mais font partie de la fiabilité :

« L’espérance n’est pas une stratégie. » — Général Gordon R. Sullivan

Il ne parlait pas des pièges de focus. Il aurait pu.

Architecture : superposition, panneau et état « source de vérité »

L’architecture minimale qui se comporte bien :

  • Bouton de bascule (généralement dans l’en-tête) : contrôle l’état ; a un nom accessible clair ; reflète ouvert/fermé.
  • Fond/superposition : couvre le viewport ; intercepte les clics/taps ; tamise éventuellement l’arrière-plan ; mécanisme de fermeture.
  • Panneau du tiroir : élément off-canvas qui glisse ; contient la navigation ; a un bouton de fermeture en haut.
  • Gestionnaire d’état : un booléen unique, plus une petite quantité de méta-données (ce qui avait le focus avant l’ouverture ; position de défilement sauvegardée).

Gardez l’état ennuyeux. Vous ne construisez pas une base de données distribuée. Dès que vous introduisez « partiellement ouvert », « ouverture par glissement », « ouvert par survol », vous passerez vos week-ends à chasser des cas limites sur des appareils que vous ne possédez pas.

Modélisez votre tiroir comme une modale (sans complexifier)

Utilisez un petit état en esprit, même si vous l’implémentez avec quelques flags :

  • Fermé : pas de superposition ; défilement du body normal ; focus normal.
  • Ouverture : définir inert/verrouillage du défilement d’abord, puis animer. Empêcher les entrées pendant la transition.
  • Ouvert : piéger le focus ; superposition active ; fermer au clic sur la superposition, bouton de fermeture, Échap et changement de route.
  • Fermeture : libérer le piège de focus après la fin de l’animation ; restaurer le défilement ; restaurer le focus.

L’ordre importe. Si vous animez d’abord et verrouillez ensuite, les utilisateurs pourront défiler la page derrière le tiroir pendant l’animation. Ils le feront. Immédiatement.

Petite blague, puisque l’on parle d’état UI

Il y a deux problèmes difficiles en informatique : invalidation de cache, nommer les choses, et fermer le tiroir de navigation lors d’un changement de route.

Superposition et contextes d’empilement : pourquoi votre fond est sous l’en-tête

Quand la superposition ne couvre pas tout, ce n’est presque jamais une question de « z-index trop bas » isolée. C’est un problème de contextes d’empilement.

Comment vous créez un contexte d’empilement par accident

Chacun des éléments suivants sur un ancêtre peut faire en sorte que votre superposition se comporte comme si elle était coincée derrière d’autres UI :

  • transform (y compris transform: translateZ(0) utilisé comme astuce « performance »)
  • filter / backdrop-filter
  • opacity < 1
  • position associé à z-index dans certains agencements
  • isolation: isolate
  • will-change (oui, cela peut pousser des éléments dans leurs propres couches et changer le comportement de composition)

Sur les sites de documentation, le coupable habituel est un en-tête fixe avec transform activé pour défilement fluide ou micro-animations. Alors votre « superposition globale » n’est plus globale.

Conseils pratiques

  • Rendre la superposition et le tiroir à la racine du document (portal vers document.body), pas à l’intérieur d’un conteneur transformé.
  • Utiliser une couche d’empilement dédiée au niveau supérieur : par exemple, créer #ui-layer comme dernier enfant du body. Éviter d’imbriquer dans les wrappers de layout.
  • Ne comptez pas sur des numéros magiques de z-index. Mettez en place un petit système : header 10, drawer 100, modal 1000. Puis respectez-le.

La superposition doit bloquer les événements pointeur de façon fiable

Utilisez la superposition comme un élément réel qui capture les événements pointeur. « Tamiser l’arrière-plan » en définissant la couleur de fond de la superposition, pas en appliquant l’opacité à toute la page. Si vous faites fondre la page entière, vous faites aussi fondre le texte ; les lecteurs d’écran s’en moqueront, mais les humains non.

Verrouillage du défilement : verrouillage du body, iOS Safari et le piège de « position: fixed »

Le verrouillage du défilement est l’endroit où la plupart des tiroirs meurent. Les navigateurs desktop pardonnent souvent des verrous approximatifs. Les navigateurs mobiles se souviennent et vous punissent ensuite avec du rebond, du contenu saccadé et le redoutable « la page revient en haut à la fermeture ».

Ce que vous cherchez à obtenir

Quand le tiroir est ouvert :

  • Le document derrière la superposition ne doit pas défiler.
  • Le panneau du tiroir lui-même peut défiler (les listes de navigation sont longues).
  • Le défilement tactile à l’intérieur du tiroir ne doit pas « enchaîner » sur le body.
  • À la fermeture, rendre le body exactement à la position de défilement d’avant.

Le pattern robuste : verrouiller le body avec position fixed et scrollY sauvegardé

C’est le pattern qui fonctionne sur la plupart des versions de Mobile Safari :

  • Capturer scrollY à l’ouverture.
  • Définir le body en position: fixed, top: -scrollY, left: 0, right: 0, width: 100%.
  • À la fermeture, supprimer ces styles et restaurer le défilement avec window.scrollTo(0, savedScrollY).

Oui, ça ressemble à une astuce. C’en est une. Mais c’est une astuce stable, et c’est ce pour quoi on paie.

Pourquoi pas simplement overflow: hidden sur le body ?

Parce que Mobile Safari est incohérent sur le défilement du body : parfois le conteneur de défilement est l’élément html, parfois c’est le body, parfois ça dépend si vous avez défini une hauteur. Vous pouvez faire paraître overflow: hidden fonctionnel sur votre appareil et toujours livrer une expérience cassée à quelqu’un d’autre.

Empêcher l’enchaînement de défilement avec overscroll-behavior

Pour le conteneur de défilement interne du tiroir :

  • Utilisez overscroll-behavior: contain quand c’est supporté pour empêcher l’enchaînement du défilement vers le body.
  • Sur iOS, envisagez aussi -webkit-overflow-scrolling: touch pour un défilement fluide, mais testez — certaines combinaisons avec body fixe peuvent encore jitter.

Hauteur du viewport : arrêtez d’utiliser 100vh brut sur mobile

Préférez les unités dynamiques de viewport (dvh) quand elles sont disponibles, et fournissez des fallback. Pour la hauteur du tiroir, une approche courante est height: 100dvh avec un fallback vers 100vh. Si votre pipeline CSS le permet, vous pouvez améliorer progressivement.

Deuxième blague (et dernière, par politesse et pour ma dignité) : Mobile Safari est le seul endroit où « ça marche sur mon téléphone » n’est pas une affirmation rassurante.

Gestion du focus et accessibilité : piéger, restaurer et échapper

Les utilisateurs de docs incluent des personnes qui utilisent le clavier, des lecteurs d’écran et des gens qui préfèrent simplement ne pas toucher l’écran toute la journée. Si votre tiroir casse le focus, il casse le site pour eux.

Contrat minimum d’accessibilité viable

  • Le bouton de bascule a un nom accessible (par ex. « Ouvrir la navigation »).
  • Le bouton reflète l’état avec aria-expanded="true/false".
  • Le tiroir a un conteneur sémantique : généralement nav ou un div avec un label accessible.
  • À l’ouverture, le focus se déplace dans le tiroir (typiquement sur le premier élément focalisable ou sur le bouton de fermeture).
  • Le focus est piégé à l’intérieur du tiroir tant qu’il est ouvert.
  • À la fermeture, le focus revient au bouton de bascule.
  • Échap ferme le tiroir.

Utilisez inert quand vous le pouvez

Définir le reste de la page en inert lorsque le tiroir est ouvert est la façon la plus propre d’empêcher le focus et les clics en arrière-plan. Ce n’est pas magique : vous devez toujours gérer le verrouillage du défilement, et vous avez toujours besoin d’une superposition visible pour les taps. Mais inert réduit fortement les cas limites étranges liés au focus.

Si vous ne pouvez pas compter sur inert, vous pouvez l’approcher en :

  • Ajoutant aria-hidden="true" au contenu principal pendant que le tiroir est ouvert (mais attention : masquer trop peut enlever du contexte utile aux aides techniques).
  • Utilisant une implémentation de focus trap qui cyclique le focus dans le tiroir.
  • Désactivant les pointer-events sur le contenu principal (pointer-events: none) tant que la superposition est active.

Pièges de focus : les trois règles qui évitent 90 % des bugs

  1. Le piège doit s’activer après que le tiroir est dans le DOM et visible. Sinon le premier Tab peut s’échapper.
  2. Le piège doit se désactiver même si le tiroir est fermé via le routage. Les changements de route sont là où les pièges pourrissent.
  3. Restaurer le focus systématiquement. Les utilisateurs comptent dessus. C’est aussi un excellent canari pour « est-ce que notre logique de fermeture a bien tourné ? »

Préférence de mouvement réduit n’est pas un « agrément »

Si l’utilisateur préfère un mouvement réduit, ne faites pas glisser un panneau plein écran à travers le viewport. Passez à une transformation presque instantanée ou à un fondu. L’animation du tiroir est décorative ; la navigation est le produit.

Routage, hydration et cycle de vie : fermer le tiroir au bon moment

Les sites de documentation fonctionnent souvent comme des SPA ou des apps hybrides. Ça change la façon dont les tiroirs échouent :

  • Écart d’hydratation : le HTML est rendu, l’utilisateur tape rapidement sur le menu, les handlers JS ne sont pas encore attachés. Résultat : rien ne se passe, ou la page défile.
  • Transitions de route : le clic de navigation change le contenu de la page mais laisse l’UI globale dans un état incohérent (tiroir ouvert, body verrouillé).
  • Restauration du défilement : les frameworks restaurent parfois le défilement lors d’un changement de route, ce qui se bat avec votre logique de restauration du verrouillage.

Règles pour rester sain d’esprit

  • Fermer au changement de route en s’abonnant aux événements du routeur. C’est non négociable.
  • Fermer au changement de breakpoint : lors du passage de la mise en page mobile à desktop, forcer la fermeture et déverrouiller le défilement.
  • Être défensif à la destruction : si le composant est démonté alors qu’il est ouvert, le nettoyage doit néanmoins restaurer les styles du body et l’état du focus.

Hydratation : éviter le « bouton mort »

Pour les docs rendues statiquement, envisagez :

  • Rendre le bouton de bascule comme un vrai <button> avec un script minimal inline pour ouvrir/fermer même avant l’hydratation complète (si votre plateforme le permet).
  • Ou retarder l’affichage du bouton jusqu’à ce que le JS soit prêt (moins idéal ; ça masque brièvement la navigation).

Opinion : si vos docs sont la porte d’entrée de votre produit, ne livrez pas un tiroir qui dépend d’un payload d’hydratation de 300 KB pour fonctionner.

Performance : ce qu’il ne faut pas animer, et pourquoi le flou est coûteux

Les tiroirs sont des pièges de performance parce qu’ils paraissent simples. Ils ne le sont pas. Le tiroir classique touche au layout, à la composition, au défilement et à la gestion des événements en même temps.

Animer les transforms, pas le layout

Utilisez transform: translateX() pour le panneau, pas left ou width. Les animations basées sur le layout peuvent provoquer des reflows et repaint répétés sur toute la page — particulièrement douloureux sur des docs longues avec blocs de code et coloration syntaxique.

Flou d’arrière-plan : considérez-le comme une dépendance de production

backdrop-filter: blur() rend bien. Il force aussi le navigateur à re-rasteriser constamment ce qui est derrière la superposition. Sur certains appareils, ce n’est pas « un peu plus lent ». C’est « les images tombent sous 30 fps et l’UI semble cassée ».

Si vous devez absolument l’utiliser :

  • Rendez-le conditionnel selon les préférences de transparence/mouvement réduit.
  • Limitez le rayon de flou.
  • Testez sur des Android milieu de gamme et d’anciens iPhones, pas seulement sur votre portable.

Listeners d’événements : ne fuyez pas, ne dupliquez pas

Le code du tiroir ajoute souvent des écouteurs keydown et touchmove. Si vous les attachez à chaque ouverture et oubliez de les nettoyer, vous finirez avec plusieurs handlers qui se déclenchent. Les symptômes incluent double-fermeture, jitter et pics CPU bizarres. En production, cela ressemble à « le site de docs s’aggrave au fil du temps ».

Trois mini-récits d’entreprise depuis le terrain

Incident : la mauvaise hypothèse (« overflow: hidden suffit »)

Une équipe a livré une expérience docs rafraîchie avec un tiroir qui glisse élégant. Sur desktop c’était parfait. Android Chrome allait bien. L’équipe s’est félicitée et est passée aux objectifs du trimestre suivant, ce qui est la meilleure façon d’inviter le chaos.

Dans la journée, le support a commencé à recevoir des rapports : « Le menu s’ouvre, mais la page derrière défile. Puis à la fermeture le menu me téléporte ailleurs. » Les rapports venaient surtout d’utilisateurs iPhone, mais pas uniquement. La première hypothèse de l’équipe fut que c’était un problème de z-index, parce que c’est le bug de tiroir que tout le monde connaît.

La vraie cause racine était une ligne : body { overflow: hidden; } basculée à l’ouverture. Ça « fonctionnait » sur leurs appareils de test et échouait chez d’autres à cause des différences de comportement du conteneur de défilement et de la dynamique de la barre d’adresse. Certains utilisateurs avaient le tiroir ouvert tandis que la page riait en rebond ; d’autres voyaient le body se déverrouiller à des moments étranges parce que la route changeait et que le composant était démonté sans nettoyage.

La correction a exigé deux changements : (1) passer au verrouillage du body fixe avec scrollY sauvegardé, et (2) implémenter un nettoyage global aux destructions et aux changements de route. La partie amusante : une fois le verrouillage corrigé, un bug de focus caché est apparu parce que des éléments d’arrière-plan restaient tabulables. Le tiroir comptait sur « les utilisateurs touchent l’écran », ce qui n’est pas une stratégie de focus.

L’incident n’a pas été catastrophique, mais il a entaché la réputation. Les docs sont l’endroit où les utilisateurs vont quand ils sont déjà frustrés. Casser la navigation sur mobile, c’est comme verrouiller la sortie de secours et faire semblant d’être surpris.

Optimisation qui s’est retournée : « GPU-accélérer tout »

Une autre organisation voulait que leurs docs « se sentent natives ». Quelqu’un a ajouté transform: translateZ(0) et will-change: transform à l’en-tête et à plusieurs wrappers de layout pour « améliorer le scrolling ». Le tiroir overlay et panneau étaient imbriqués dans l’un de ces wrappers, parce que c’est comme ça que l’arbre de composants était structuré.

Ça paraissait fluide dans le scénario heureux. Puis des utilisateurs ont signalé que taper hors du tiroir ne le fermait pas — parfois. Certains taps traversaient la superposition vers des liens derrière elle. Sur certaines pages, la superposition ne couvrait même pas l’en-tête. Ça a aussi cassé les tests de capture d’écran parce que la composition pixel variait entre les runs.

La cause racine : « l’optimisation » a créé un nouveau contexte d’empilement et un comportement de couche de composition qui a changé l’ordre de hit testing. La superposition avait un z-index élevé à l’intérieur de son contexte, mais le contexte lui-même était sous le contexte séparé de l’en-tête fixe. Sur quelques navigateurs, le compositeur traitait la superposition comme visuellement au-dessus tout en laissant passer les événements pointeur vers l’en-tête. C’est un cas de malédiction particulier.

Le retour arrière a supprimé la plupart des hints will-change. L’amélioration réelle de la performance est venue plus tard, en réduisant le poids du DOM dans l’arbre de nav et en différant la coloration syntaxique pendant les temps d’inactivité. L’animation du tiroir n’a jamais été le goulot d’étranglement ; c’était la page.

Pratique ennuyeuse mais correcte qui a sauvé la mise : contrat de nettoyage

Une troisième équipe avait une règle stricte : tout composant qui mutait l’état global devait fournir un chemin de nettoyage explicite, testé par l’automatisation. Le tiroir mutait l’état global : styles du body pour le verrouillage du défilement, inert/aria-hidden pour le contenu principal, et handlers de touches au niveau document.

Ils ont écrit un petit module « UI lock » qui possédait ces mutations. Les composants pouvaient demander un verrou avec un token ; la libération du token restaurait l’état antérieur seulement lorsque le dernier token était libéré. Ce n’était pas glamour. C’était, néanmoins, résistant à la ré-entrance et aux démontages lors de changements de route.

Des mois plus tard, une mise à jour du routeur a changé le timing des événements de transition de route. Chez d’autres équipes, les tiroirs ont commencé à laisser le body verrouillé. Chez cette équipe, le contrat de nettoyage a continué de s’exécuter parce qu’il était connecté à la fois au unmount et aux événements de fin de route, et parce que leur test E2E vérifiait qu’après la navigation le body n’avait pas de position fixe et que le focus était dans le contenu.

Personne n’a envoyé d’email de célébration à ce sujet. Bien sûr que non. Une UI fiable est comme un stockage fiable : si les gens la remarquent, quelque chose a déjà mal tourné.

Mode d’emploi pour un diagnostic rapide

Quand le tiroir est cassé en production, vous voulez un chemin rapide vers le goulot. Voici l’ordre qui minimise les pertes de temps.

1) Est-ce un problème de stacking / de superposition ?

  • Vérifier : La superposition couvre-t-elle visuellement tout ? Intercepte-t-elle les taps ?
  • Signal : Si les taps traversent, ou si l’en-tête est au-dessus de la superposition, suspectez les contextes d’empilement et le placement du portail.
  • Direction de correction immédiate : Déplacez l’overlay/panel dans un portail racine ; retirez transform/filter des ancêtres ; standardisez les couches de z-index.

2) Est-ce un problème de verrouillage du défilement ?

  • Vérifier : Avec le tiroir ouvert, pouvez-vous défiler la page derrière ? La fermeture fait-elle sauter la position de défilement ?
  • Signal : Saut en haut après la fermeture est classique pour un mauvais usage de overflow: hidden ou un mauvais lock de body.
  • Direction de correction immédiate : Passez au verrouillage fixe du body avec position sauvegardée ; assurez-vous d’un nettoyage sur chaque chemin de fermeture.

3) Est-ce un problème de focus / clavier ?

  • Vérifier : Avec un clavier, Tab s’échappe-t-il ? Échap ferme-t-il ? Le focus revient-il au bouton après la fermeture ?
  • Signal : L’évasion du focus signifie généralement que le piège n’est pas activé assez tôt ou que le fond n’est pas inert.
  • Direction de correction immédiate : Ajoutez inert ou aria-hidden ; implémentez un focus trap fiable ; restaurez explicitement le focus.

4) Est-ce un problème de performance / jank ?

  • Vérifier : L’ouverture du tiroir fait-elle chuter les frames ou geler ? Le CPU monte-t-il ?
  • Signal : Flou d’arrière-plan, DOM de nav volumineux, reflows forcés, ou duplication des écouteurs d’événements.
  • Direction de correction immédiate : Supprimez le flou, animez uniquement les transforms, réduisez le DOM, auditez les écouteurs et reflows.

Erreurs courantes : symptôme → cause racine → correction

La superposition ne couvre pas l’en-tête

Symptôme : L’en-tête fixe reste cliquable/visible au-dessus de l’arrière-plan tamisé.
Cause racine : La superposition est à l’intérieur d’un contexte d’empilement inférieur ; l’en-tête a créé un contexte via transform/filter ou des règles z-index.
Correction : Rendre overlay/panel via un portail à la fin du body ; retirer le CSS qui crée des contextes d’empilement des wrappers de layout ; définir une échelle de z-index.

Les taps « fuient » à travers la superposition

Symptôme : Cliquer en dehors du tiroir déclenche des liens derrière.
Cause racine : La superposition a pointer-events: none, ou elle ne couvre pas le viewport, ou mismatch de compositing/hit-testing dû aux transforms.
Correction : Assurez-vous que la superposition est position: fixed; inset: 0; avec pointer-events activés ; évitez l’imbrication dans des parents transformés.

Le fond défile sous le tiroir ouvert

Symptôme : Vous pouvez défiler la page pendant que le tiroir est ouvert ; effet de rebond apparait.
Cause racine : Utiliser seulement overflow: hidden ou verrouiller le mauvais élément ; enchaînement de défilement depuis le conteneur du tiroir vers le body.
Correction : Utiliser le verrouillage fixe du body avec position sauvegardée ; définir overscroll-behavior: contain sur la zone de défilement du tiroir.

La fermeture du tiroir fait sauter la page en haut

Symptôme : Fermer le tiroir et votre position de défilement se réinitialise ou se déplace.
Cause racine : Le body a été mis en position: fixed sans restauration du scroll ; ou la restauration du scroll du framework se bat avec votre logique ; ou vous avez modifié la hauteur html/body.
Correction : Sauvegarder scrollY à l’ouverture ; définir top: -scrollY ; à la fermeture, enlever les styles et appeler window.scrollTo(0, savedY). Intégrer avec la restauration de scroll du routeur.

Le focus clavier s’échappe dans la page derrière

Symptôme : Appuyer sur Tab et le focus va sur des liens du contenu principal, pas le tiroir.
Cause racine : Pas de piège de focus, ou le piège s’active trop tard, ou le fond est encore focalisable.
Correction : Utiliser un focus trap et l’activer immédiatement à l’ouverture ; définir inert sur le contenu principal ; restaurer le focus à la fermeture.

Échap ferme le tiroir parfois, mais pas toujours

Symptôme : Échap fonctionne une fois, puis s’arrête, ou ne marche que sur certaines pages.
Cause racine : Écouteur keydown attaché à un composant qui se démonte ; écouteurs dupliqués ; le focus est dans un iframe/bloc de code qui capture les touches.
Correction : Attacher keydown au niveau document pendant l’ouverture ; assurer le nettoyage ; ignorer Échap si l’utilisateur compose avec un IME ; gérer le focus dans les widgets embarqués.

Le tiroir s’ouvre mais le lecteur d’écran annonce n’importe quoi

Symptôme : Le lecteur d’écran n’annonce pas la navigation, ou lit le contenu de fond pendant que le tiroir est ouvert.
Cause racine : Étiquettes manquantes, mauvaise utilisation de aria-hidden, absence de gestion du focus, ou fond non inert.
Correction : Labelliser la nav ; déplacer le focus dedans à l’ouverture ; inert ou masquer correctement le fond ; s’assurer que le bouton de fermeture est atteignable et nommé.

L’ouverture du tiroir est saccadée

Symptôme : Chute de frames, lag au toucher, démarrage d’animation retardé.
Cause racine : Animation de propriétés de layout ; flou d’arrière-plan lourd ; forcer des layouts synchrones via des lectures/écritures JS ; DOM volumineux dans la nav.
Correction : Animer les transforms ; supprimer ou réduire le flou ; grouper lectures/écritures DOM ; virtualiser ou réduire les sections de nav.

Tâches en production avec commandes : vérifier, mesurer, décider

Les bugs UI apparaissent comme des « problèmes front-end », mais les diagnostiquer bénéficie de la même discipline que pour le stockage et la fiabilité : mesurer d’abord, changer ensuite, vérifier enfin. Ci-dessous des tâches pratiques que vous pouvez exécuter depuis un shell en reproduisant les problèmes dans un environnement de staging ou proche de la production.

Hypothèses : vous avez accès à un hôte de test qui exécute le site de docs, aux logs, et éventuellement à un environnement navigateur headless. Les commandes sont réalistes et exécutables ; adaptez les chemins et noms de service.

Tâche 1 : Confirmer quel HTML est servi (SSR vs client-only)

cr0x@server:~$ curl -sS -D- https://docs.internal.example/guide/install | head -n 30
HTTP/2 200
content-type: text/html; charset=utf-8
cache-control: public, max-age=0, must-revalidate
x-powered-by: app
...
<!doctype html>
<html lang="en">
...

Ce que signifie la sortie : Vous vérifiez les en‑têtes de réponse et si du contenu HTML est livré. Si vous voyez du HTML essentiellement vide avec une grosse référence à un bundle script et pas de balisage de navigation, votre tiroir dépend de l’hydratation.
Décision : Si le tiroir dépend de l’hydratation, priorisez la mitigation « pas de bouton mort » (script minimal inline ou shell de nav rendu côté serveur).

Tâche 2 : Vérifier que la mise en cache ne sert pas des versions JS/CSS incompatibles

cr0x@server:~$ curl -sSI https://docs.internal.example/assets/app.css | egrep -i 'cache-control|etag|last-modified'
cache-control: public, max-age=31536000, immutable
etag: "a9f4c2-18b10"
last-modified: Tue, 10 Dec 2024 18:22:11 GMT

Ce que signifie la sortie : La mise en cache longue durée va si les assets sont content-hashés et que le HTML référence les bonnes versions.
Décision : Si le HTML pointe vers des assets non-hashés avec des durées de cache longues, vous pouvez avoir « JS du tiroir ne correspond pas au HTML/CSS ». Corrigez les en-têtes de cache ou la versioning des assets.

Tâche 3 : Inspecter la Content Security Policy impactant les scripts inline pour l’interactivité précoce

cr0x@server:~$ curl -sSI https://docs.internal.example/ | egrep -i 'content-security-policy'
content-security-policy: default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'

Ce que signifie la sortie : Si vous prévoyez d’utiliser de petits scripts inline pour éviter les gaps d’hydratation, le CSP peut les bloquer.
Décision : Soit gardez le comportement du tiroir entièrement dans le bundle JS, soit mettez à jour le CSP avec une stratégie basée sur nonce. N’ajoutez pas « unsafe-inline » à la va-vite.

Tâche 4 : Trouver des pics d’erreurs liés aux interactions du tiroir (erreurs client ingérées côté serveur)

cr0x@server:~$ sudo journalctl -u docs-web -S "2 hours ago" | egrep -i 'TypeError|Unhandled|focus|inert|scroll' | tail -n 20
Dec 29 12:11:04 web-01 docs-web[2410]: UnhandledRejection: TypeError: Cannot read properties of null (reading 'focus')
Dec 29 12:14:19 web-01 docs-web[2410]: TypeError: Failed to execute 'setAttribute' on 'HTMLElement': 'inert' is not a valid attribute name

Ce que signifie la sortie : Les bugs de restauration du focus et l’usage incorrect d’inert peuvent lancer des exceptions, qui empêchent le nettoyage de s’exécuter et entraînent « le tiroir ne se ferme pas ».
Décision : Traitez-les comme des problèmes de fiabilité : ajoutez des gardes, assurez le nettoyage dans des blocs finally, et détectez la présence d’inert correctement.

Tâche 5 : Confirmer que l’élément overlay existe et n’est pas supprimé/minifié par erreur

cr0x@server:~$ curl -sS https://docs.internal.example/ | grep -n 'data-testid="nav-overlay"' | head
184:    <div data-testid="nav-overlay" class="overlay" hidden></div>

Ce que signifie la sortie : Vous vérifiez que la superposition est effectivement dans le DOM tel que livré (SSR) ou au moins présente dans le template.
Décision : Si elle manque, déboguer le CSS ne servira à rien. Corrigez d’abord le rendu/le templating.

Tâche 6 : Vérifier la présence de bundles JS trop volumineux qui retardent l’hydratation

cr0x@server:~$ curl -sSI https://docs.internal.example/assets/app.js | egrep -i 'content-length|content-encoding'
content-encoding: br
content-length: 612341

Ce que signifie la sortie : Un bundle brotli ~600 KB peut être lourd sur mobile, surtout à cause du coût de parse/compile.
Décision : Si la fonctionnalité du tiroir attend ce bundle, fractionnez l’UI critique, différer les scripts non essentiels et évitez de faire tenir la nav en otage à l’analytics.

Tâche 7 : Valider la compression serveur pour CSS/JS (transfert lent = fenêtre « bouton mort » plus longue)

cr0x@server:~$ curl -sSI -H 'Accept-Encoding: gzip, br' https://docs.internal.example/assets/app.js | egrep -i 'content-encoding|vary'
vary: Accept-Encoding
content-encoding: br

Ce que signifie la sortie : La compression est activée et varie correctement par encodage.
Décision : Si la compression manque, corrigez cela avant de repenser le tiroir. La latence est un flag fonctionnel que vous avez oublié d’activer.

Tâche 8 : Vérifier que les routes du tiroir ferment le tiroir (les logs serveur pour SPA ne suffisent pas ; utilisez des checks synthétiques)

cr0x@server:~$ node -e "console.log('Run an E2E check here with Playwright/Cypress in CI; server logs cannot see client route changes.')"
Run an E2E check here with Playwright/Cypress in CI; server logs cannot see client route changes.

Ce que signifie la sortie : Rappel brutal : vous ne pouvez pas diagnostiquer des bugs d’état client uniquement depuis des logs serveur.
Décision : Ajoutez un contrôle navigateur synthétique qui ouvre le tiroir, clique sur un lien de nav, et affirme que le body est déverrouillé et que le focus est dans le contenu.

Tâche 9 : Chercher une mauvaise config NGINX qui casse les range requests (impacte la perf mobile)

cr0x@server:~$ curl -sSI https://docs.internal.example/assets/app.js | egrep -i 'accept-ranges'
accept-ranges: bytes

Ce que signifie la sortie : Les range requests peuvent aider certains clients et CDN. Ce n’est pas un bug de tiroir, mais ça affecte le time-to-interactive, qui impacte la réactivité du tiroir.
Décision : Si absent, revoyez la config de service des assets statiques. Les régressions de perf se manifestent comme « le menu est cassé » parce que les utilisateurs tapent avant que les handlers existent.

Tâche 10 : Confirmer le temps de réponse et la latence en queue sur les pages lourdes en tiroir

cr0x@server:~$ curl -sS -w "ttfb=%{time_starttransfer} total=%{time_total}\n" -o /dev/null https://docs.internal.example/guide/reference
ttfb=0.142315 total=0.211904

Ce que signifie la sortie : Si le TTFB est élevé, votre HTML arrive tard. Si le temps total est élevé, le chemin réseau est lent. Les deux rallongent la sensation d’UI morte.
Décision : Si le TTFB grimpe, corrigez le cache backend/render. Si le réseau est lent, améliorez CDN, compression et stratégie d’assets.

Tâche 11 : Surveiller la pression mémoire sur le serveur causant des réponses lentes (ce n’est pas toujours le front-end)

cr0x@server:~$ free -h
               total        used        free      shared  buff/cache   available
Mem:           31Gi        26Gi       1.2Gi       352Mi       3.9Gi       4.3Gi
Swap:          2.0Gi       1.8Gi       256Mi

Ce que signifie la sortie : Peu de mémoire disponible et swap utilisé peuvent provoquer des pics de latence en servant HTML/JS, retardant l’interactivité.
Décision : Si swap utilisé, corrigez la pression mémoire avant de blâmer le CSS. Les utilisateurs se moquent de l’endroit où se situe le bug.

Tâche 12 : Identifier la saturation CPU lors des pics d’usage docs (encore : « menu non réactif » peut être côté serveur)

cr0x@server:~$ uptime
 12:29:44 up 18 days,  4:12,  2 users,  load average: 6.21, 6.02, 5.88

Ce que signifie la sortie : Une charge élevée relative aux cœurs CPU peut ralentir la livraison HTML et JS.
Décision : Si la charge est constamment élevée, scalez, mettez en cache ou réduisez le coût de rendu. Puis retestez la « réactivité » du tiroir.

Tâche 13 : Vérifier la taille des assets statiques sur le disque pour repérer des builds debug livrés par erreur

cr0x@server:~$ ls -lh /var/www/docs/assets | egrep 'app\.(js|css)' | head -n 5
-rw-r--r-- 1 www-data www-data 5.9M Dec 10 18:22 app.js
-rw-r--r-- 1 www-data www-data 412K Dec 10 18:22 app.css

Ce que signifie la sortie : Si app.js fait plusieurs méga-octets non compressés, vous livrez peut-être des source maps ou des builds dev en production.
Décision : Corrigez la pipeline de build. Les bugs de tiroir se multiplient quand le client peine à parser votre roman JavaScript.

Tâche 14 : Vérifier si les pages d’erreur ou redirections d’auth sont mises en cache (le tiroir « casse » parce que la page n’est pas la page)

cr0x@server:~$ curl -sSI https://docs.internal.example/guide/install | egrep -i 'http/|location:|cache-control:'
HTTP/2 200
cache-control: public, max-age=0, must-revalidate

Ce que signifie la sortie : Si vous obtenez des redirections ou des en-têtes de cache inattendus, les clients peuvent voir du HTML partiel ou obsolète (manquant les scripts de nav).
Décision : Assurez-vous d’un comportement de cache correct pour le HTML et d’un bon traitement des redirections. Un JS de tiroir manquant parce qu’une interstitielle de login a été servie reste une panne de tiroir.

Tâche 15 : Valider que votre service worker (si présent) ne sert pas un shell HTML obsolète

cr0x@server:~$ grep -R "workbox" -n /var/www/docs/ | head
/var/www/docs/sw.js:12:importScripts('workbox-*.js');

Ce que signifie la sortie : Un service worker peut cacher agressivement l’app shell et servir du HTML/JS dépareillés entre les releases.
Décision : Si vous utilisez un service worker, implémentez un versioning et une invalidation de cache corrects. Sinon vous déboguerez des régressions « aléatoires » du tiroir qui sont en fait des clients obsolètes.

Tâche 16 : Chercher des attachements dupliqués de handlers d’événements dans le bundle construit (grep rapide)

cr0x@server:~$ grep -R "addEventListener(\"keydown\"" -n /var/www/docs/assets/app.js | head
12877:document.addEventListener("keydown",u)

Ce que signifie la sortie : Ce n’est pas la preuve d’une fuite, mais ça vous indique où les handlers clavier sont câblés. Si votre app ajoute des écouteurs à chaque ouverture sans les retirer, vous verrez plusieurs appels à l’exécution.
Décision : Auditez le cycle de vie et le nettoyage. Ajoutez des compteurs d’instrumentation si besoin. Les fuites d’événements UI sont la cousine des fuites de descripteurs de fichier : ignorées jusqu’à ce qu’elles mordent.

Listes de contrôle / plan étape par étape

Plan d’implémentation étape par étape (la version ennuyeuse qui fonctionne)

  1. Placer la superposition et le tiroir dans une couche UI de haut niveau appendue au body (portal). Éviter les ancêtres transformés.
  2. Créer une échelle de z-index et l’appliquer en revue de code. Si quelqu’un ajoute z-index: 999999, demandez-lui d’expliquer ses choix de vie.
  3. Implémenter le verrouillage du défilement en utilisant scrollY sauvegardé + body fixe. Inclure le nettoyage sur chaque chemin de sortie.
  4. Faire du panneau du tiroir son propre conteneur de défilement avec containment overscroll.
  5. Implémenter la gestion du focus : sauvegarder l’élément actif, déplacer le focus dans le tiroir à l’ouverture, piéger le focus, restaurer à la fermeture.
  6. Implémenter les mécanismes de fermeture : clic sur la superposition, bouton de fermeture, Échap, changement de route, changement de breakpoint.
  7. Ajouter le support du mouvement réduit pour éviter les transitions lourdes en mouvement.
  8. Tester sur iOS Safari avec des pages longues et une position de défilement profonde. N’acceptez pas « ça marche dans l’émulateur Chrome ».
  9. Ajouter des tests E2E vérifiant la restauration du verrouillage et du focus à travers la navigation.
  10. Instrumenter les erreurs provenant des chemins focus/scroll ; traitez-les comme des incidents d’accessibilité/dispersion.

Checklist pré-merge (ce qu’il faut exiger en revue)

  • La superposition est position: fixed et couvre tout le viewport.
  • Pas de superposition/tiroir à l’intérieur d’un wrapper de layout transformé.
  • Le verrouillage du body stocke et restaure la position de défilement de façon déterministe.
  • Tous les chemins de fermeture appellent la même fonction de nettoyage.
  • Le focus est restauré sur le bouton de bascule après la fermeture.
  • Le tiroir a un bouton de fermeture atteignable au clavier.
  • Échap ferme ; Tab ne s’échappe pas.
  • La préférence de mouvement réduit est respectée.
  • Le changement de route ferme le tiroir et déverrouille le défilement.
  • Pas de blur/backdrop-filter sans validation de perf signée.

Checklist de release (réalité production)

  • Vérifier les en‑têtes de cache : HTML pas trop mis en cache ; assets immuables avec hashes.
  • Vérifier que les budgets de taille de bundles n’ont pas régressé le time-to-interactive.
  • Lancer un test mobile synthétique de navigation en CI et après le déploiement.
  • Surveiller l’ingestion d’erreurs client pour focus/scroll exceptions.
  • Avoir une procédure de rollback qui invalide aussi le cache du service worker si utilisé.

FAQ

1) Un tiroir de navigation pour la doc devrait-il être un <dialog> ?

Généralement non. Traitez-le comme une modale en comportement, mais sémantiquement c’est de la navigation. Utilisez un conteneur nav, une superposition et un piégeage du focus. <dialog> peut fonctionner, mais il apporte ses propres particularités et contraintes de style.

2) inert est-il sûr à utiliser ?

Il est désormais largement utilisable, mais il faut quand même le détecter et le tester. Si vous ne pouvez pas y compter partout où vous en avez besoin, retombez sur un focus trap plus une gestion aria prudente. Ne livrez pas quelque chose qui bloque les clics mais laisse le fond focalisable.

3) Pourquoi ma page saute quand je ferme le tiroir ?

Parce que votre verrouillage du défilement n’a pas restauré la position correctement, ou parce que la restauration de scroll du framework s’est battue avec vous. Sauvegardez scrollY à l’ouverture, verrouillez le body avec position fixe et top: -scrollY, puis restaurez scrollY à la fermeture et coordonnez-vous avec le comportement de scroll du routeur.

4) Dois-je vraiment piéger le focus pour un tiroir de nav ?

Oui, si le tiroir se comporte comme une superposition qui bloque la page. Sinon, les utilisateurs clavier peuvent tabuler vers du contenu invisible ou masqué derrière le tiroir. Ce n’est pas « légèrement agaçant », c’est cassé.

5) Pourquoi la superposition reste-t-elle sous certains éléments même avec un z-index élevé ?

Parce que le z-index est scindé par les contextes d’empilement. Si votre superposition se trouve dans un contexte d’empilement qui est sous celui d’un en-tête, elle ne peut pas le surpasser. Corrigez le placement DOM (portal) et retirez les déclencheurs de contexte d’empilement des ancêtres.

6) Le tiroir doit-il se fermer quand un lien de nav est cliqué ?

Oui. Toujours. Fermez immédiatement au clic (optimiste), puis laissez le routage se produire. Fermez aussi sur les événements de changement de route au cas où la navigation se produirait par d’autres moyens (programmatique, back/forward, hash changes).

7) Le flou d’arrière-plan en vaut-il la peine ?

Seulement si vous pouvez prouver que cela ne détruit pas la perf sur des appareils représentatifs. Le flou est un coût constant pendant l’animation et tant que l’overlay est ouvert. Une simple superposition semi-transparente est moins coûteuse et plus prévisible.

8) Comment gérer des arbres de nav imbriqués sans rendre le tiroir inutilisable ?

Utilisez la divulgation progressive : repliez les sections par défaut, conservez l’état d’expansion de l’utilisateur par session et gardez le chemin de la page active développé. Et gardez le DOM de la nav plus léger que votre ego : les arbres profonds peuvent être coûteux à rendre et à défiler.

9) Et les gestes de glissement pour ouvrir/fermer le tiroir ?

Faites attention. Les gestes entrent en conflit avec la navigation du navigateur et le défilement. Si vous implémentez un swipe, rendez-le optionnel et secondaire aux contrôles explicites. Votre travail principal est « s’ouvrir de manière fiable », pas « ressembler à une démo d’app native ».

10) Comment tester cela correctement ?

Exécutez des tests E2E qui vérifient : la superposition couvre le viewport, le fond ne défile pas, le focus se place dans le tiroir, Tab reste à l’intérieur, Échap ferme, le focus revient au toggle, et le changement de route déverrouille le défilement. Puis testez manuellement sur iOS Safari avec une page longue et un défilement profond.

Conclusion : prochaines étapes qui tiennent en production

Un tiroir mobile pour la documentation n’est pas un artifice de design. C’est une infrastructure. Il détermine si les utilisateurs peuvent quitter une page, trouver le bon guide et garder leur place pendant qu’ils déboguent à 2 h du matin — ce qui, d’ailleurs, est aussi le moment où vous déboguerez votre tiroir si vous le livrez bâclé.

Faites ceci ensuite :

  1. Déplacez la superposition/tiroir dans un portail de couche UI de haut niveau et standardisez le z-index.
  2. Implémentez le verrouillage du body fixe avec position de défilement sauvegardée et un nettoyage à toute épreuve.
  3. Ajoutez inert (ou équivalent) et un vrai focus trap ; restaurez le focus à la fermeture.
  4. Fermez au changement de route et au changement de breakpoint — à chaque fois.
  5. Lancez un test E2E synthétique qui échoue bruyamment si le body reste verrouillé ou si le focus s’échappe.

Livrez le tiroir ennuyeux. Vos utilisateurs ne vous remercieront jamais, et c’est ainsi que vous saurez que ça a marché.

← Précédent
Windows Phone : comment un bon OS a perdu face aux écosystèmes
Suivant →
WordPress bloqué en mode maintenance : le supprimer en toute sécurité et éviter les récidives

Laisser un commentaire