Les UI « sticky » sont une fonctionnalité que l’on ne remarque que quand elles foirent. Un en-tête qui disparaît en plein défilement, une barre latérale qui refuse de se fixer, un bouton « sticky » qui se colle au mauvais élément — généralement juste avant un lancement, pendant une démo, ou quand votre CEO « regarde rapidement le site sur son iPad ».
Le CSS a rendu sticky trompeusement simple, puis les systèmes en production l’ont rendu honnête. Sticky dépend des conteneurs de défilement, de la containment, des contextes d’empilement, des modes de mise en page et des bizarreries des navigateurs. Traitez-le comme un problème de fiabilité : définissez le comportement attendu, observez la chaîne de défilement réelle, puis supprimez les pièges.
Un modèle mental de production pour sticky
position: sticky n’est ni fixed ni relative. C’est un hybride qui se comporte comme relative jusqu’à un seuil, puis comme « fixé à l’intérieur d’une bordure », et cesse d’être sticky lorsqu’il atteint la fin de cette bordure.
Les trois règles qui comptent plus que tout
- Sticky s’accroche au conteneur de défilement le plus proche (plus précisément : l’ancêtre le plus proche qui crée une boîte de défilement et qui découpe l’overflow pour cet axe). Si vous pensez « la fenêtre », vous êtes déjà en difficulté.
- Sticky a besoin d’un seuil (
top,bottom,leftouright) et ce seuil doit avoir du sens selon l’axe que vous faites défiler. - Sticky est contraint par son bloc contenant. Il ne flottera pas au-delà de la fin du conteneur juste parce que vous l’avez demandé poliment.
Le rapport de bug sticky habituel dit : « ça marchait hier. » Qu’est-ce qui a changé hier ? Pas sticky. La chaîne d’ancêtres. Quelqu’un a enveloppé du contenu dans un nouveau div avec overflow: hidden, ajouté un transform pour une animation, ou basculé la mise en page en flex/grid et modifié par erreur le comportement des min-sizes. Sticky n’a pas changé. Votre containment a changé.
Définissez le comportement comme un SRE : qu’est-ce qui est « correct » ?
Écrivez explicitement :
- Quel élément doit coller (et sur quel axe).
- Où il doit se coller (
top: 0,top: 64pxsous une nav globale, etc.). - Par rapport à quoi il doit se coller (viewport vs section de page vs corps de modal).
- Quand il doit arrêter de coller (fin de la colonne d’article, fin de la région de la barre latérale, bas du modal).
Ce n’est pas du théâtre processuel. C’est comment éviter la situation la plus courante de « sticky se bat contre sticky » : un en-tête global est sticky par rapport à la fenêtre, un en-tête local est sticky par rapport à un scroll interne, et les deux réclament les mêmes pixels. Le navigateur choisira ; votre équipe produit n’appréciera pas le choix.
Sticky et la chaîne de défilement : trouvez le scroller que vous utilisez réellement
Beaucoup d’apps ne font plus défiler le document. Elles font défiler un div racine (#app), un conteneur de layout (.shell) ou le corps d’un modal. C’est acceptable — mais sticky utilisera ce scroller, pas la fenêtre, si les réglages d’overflow créent un conteneur de scroll.
Si vous ne retenez qu’un principe de diagnostic, retenez celui-ci : sticky ne casse pas au hasard ; il casse de manière déterministe parce que le conteneur de défilement et le bloc contenant ne sont pas ceux que vous croyez.
Blague 1/2 : Sticky est comme un chat — si vous essayez de le forcer, il cesse de coopérer et vous regarde jusqu’à ce que vous changiez l’environnement.
Une citation, parce que l’exploitation a des preuves
Espérer n’est pas une stratégie.
— idée paraphrasée couramment attribuée aux ingénieurs et opérateurs des cercles de fiabilité.
Faits et historique qui expliquent les bizarreries d’aujourd’hui
Les UI sticky ont l’air modernes, mais les contraintes sont anciennes. Le moteur de mise en page du navigateur négocie depuis des décennies entre « peindre ici » et « ne pas casser le défilement ».
- Fait 1 : Le comportement sticky existait comme extension WebKit (
-webkit-sticky) avant la standardisation, ce qui explique pourquoi les bizarreries historiques de Safari persistent dans certains cas limites. - Fait 2 : Les premiers « en-têtes sticky » étaient souvent des gestionnaires de scroll en JavaScript qui appliquaient
position: fixed, provoquant du jank parce que les events de scroll déclenchent beaucoup et que la mise en page thrash quand on lit/écrit le DOM en boucle. - Fait 3 : L’essor des applications single-page a poussé le défilement dans des conteneurs imbriqués pour garder le « shell de l’app » statique — créant accidentellement la raison n°1 pour laquelle sticky « cesse de fonctionner ».
- Fait 4 :
overflow: hiddenest devenu un clearfix par défaut et un hack pour « empêcher le rebond » dans beaucoup de bases de code. Il clippe aussi et établit des comportements de scroll/containment qui interfèrent avec sticky. - Fait 5 : Mobile Safari traitait historiquement les unités viewport et les barres d’outils dynamiques de façon incohérente, rendant les layouts « sticky + 100vh » un moyen sûr de perdre un après-midi.
- Fait 6 : Les transforms (
transform) créent de nouveaux blocs contenant et contextes d’empilement ; ils ont été largement utilisés pour l’accélération GPU bien avant que l’on réalise qu’ils peuvent changer le comportement contenant de sticky. - Fait 7 : Sticky à l’intérieur d’éléments de table (
thead,th) a connu un support inégal au fil des ans ; ça s’est amélioré, mais la « mise en page tableau » a toujours des comportements cas-particuliers comparés au layout bloc. - Fait 8 : L’impression et les médias paginés ont influencé tôt les moteurs de layout ; sticky ne s’y applique pas, mais l’héritage de la « fragmentation » et de la « containment » façonne encore ce que les navigateurs considèrent comme un modèle de positionnement sûr.
Patrons éprouvés : en-têtes, barres latérales et tableaux
Sticky peut être ennuyeux. Vous voulez de l’ennuyeux. L’ennuyeux se déploie.
Patron A : En-tête global sticky (défilement du document)
Utilisez ceci quand le document défile (pas de scroller imbriqué). Gardez-le simple :
- Placez l’en-tête près du haut du DOM.
- Donnez-lui
position: stickyettop: 0. - Donnez-lui un background et un z-index réfléchi.
Détail clé : Ne comptez pas sur la transparence à moins de vouloir voir le contenu transparaître au scroll. Si vous la souhaitez, ajoutez un backdrop filter subtil et acceptez les implications sur le support navigateur.
Patron B : Barre latérale sticky dans une section de page
Layout classique de docs : nav à gauche, contenu principal, colonne « sur cette page » à droite. Le piège : la barre latérale se colle par rapport au conteneur de défilement le plus proche, et le bloc contenant de la barre latérale est souvent le parent flex.
À faire :
- Faites défiler la page via le document quand c’est possible.
- Assurez-vous que la chaîne d’ancêtres de la barre latérale ne définit pas overflow sur l’axe de scroll.
- Utilisez
align-self: flex-startsi nécessaire pour éviter que le flex n’étire la hauteur de façon inattendue.
Approche fiable : enveloppez la barre latérale dans une colonne qui définit la bordure souhaitée, et appliquez sticky à un élément interne :
.sidebar-columndéfinit les frontières et les règles de hauteur..sidebar-innerest sticky avectopréglé sur la hauteur de l’en-tête.
Patron C : En-tête sticky à l’intérieur d’un conteneur défilable (modals, panneaux)
Ici, sticky est idéal car il se fixe au conteneur, pas à la fenêtre — exactement ce qu’on veut dans un modal qui a son propre scroll.
À faire :
- Faites du corps du modal le conteneur de scroll (
overflow: auto). - Placez l’élément sticky à l’intérieur de ce conteneur (pas au-dessus).
- Soyez explicite sur
top, le background et l’empilement.
Patron D : En-têtes de tableau sticky
Quand vous avez besoin d’en-têtes de tableau sticky à l’intérieur d’une boîte avec scroll :
- Utilisez un div wrapper qui scroll (
overflow: auto). - Appliquez
position: sticky; top: 0aux élémentsth. - Définissez un
backgroundsur lesthpour que les lignes ne transparaissent pas.
Vérification réaliste : Le rendu des tableaux peut devenir étrange avec z-index et collapse des bordures. Si vous avez besoin d’un rendu pixel-perfect, envisagez de séparer l’en-tête et le corps en deux tableaux synchronisés — mais seulement si vous êtes prêt à le maintenir.
Pourquoi sticky casse (les modes de défaillance fréquents)
1) Le mauvais conteneur de défilement
Si un ancêtre a overflow: auto ou overflow: hidden (ou même overflow: clip), l’élément sticky peut être contraint à cet ancêtre. C’est le désaccord n°1 entre l’intention (« coller à la fenêtre ») et le comportement réel (« coller à ce panneau »).
À quoi ça ressemble : Sticky ne fonctionne que dans une petite zone ; il s’arrête tôt ; ou il ne colle jamais parce que le conteneur ne défile pas vraiment.
2) Pas de seuil (ou un seuil équivalent à « aucun »)
Sticky a besoin d’un seuil : top ou bottom. Si vous ne le définissez pas, beaucoup de navigateurs ne feront rien de significatif. Si vous le définissez sur un axe qui ne défile jamais, rien ne se passe non plus. C’est ennuyeux, mais réel.
3) Un ancêtre crée un bloc contenant via transform/filter/perspective
Les transforms et certains effets créent de nouveaux blocs contenant et contextes d’empilement. Sticky est sensible à cela parce que le navigateur doit décider ce que signifie « relatif à » quand un parent est effectivement un nouveau système de coordonnées.
Coupables courants :
transform: translateZ(0)utilisé comme hack d’accélération GPUfilteroubackdrop-filtersur un conteneur parentperspectivesur des wrappers de layoutcontain: paintou d’autres réglages de containment utilisés pour l’isolation perf
4) Pièges Flexbox et min-height
Sticky à l’intérieur de layouts flex peut échouer quand la hauteur et le comportement d’overflow du parent créent des contraintes inattendues. Un classique est un conteneur flex en pleine hauteur avec des defaults de min-height qui empêchent le conteneur de scroller, si bien que sticky n’atteint jamais son état « collé ».
Deux règles pratiques :
- Si vous avez un layout flex en pleine hauteur, vous aurez souvent besoin de
min-height: 0sur les enfants flex qui doivent être autorisés à rétrécir et à scroller. - N’appliquez pas sticky à un élément qui est étiré de façon à rendre sa « position normale » ambiguë.
5) Surprises z-index et contextes d’empilement
Un sticky qui « fonctionne » mais est caché sous du contenu est un problème d’empilement, pas un problème sticky. Les éléments sticky ne flottent pas automatiquement au-dessus de tout. Si un sibling crée un nouveau contexte d’empilement avec un z-index plus élevé, votre en-tête sticky semblera disparaître.
6) Déplacements de layout et contenu dynamique
Sticky est calculé par rapport à la mise en page. Si du contenu se charge tard (pubs, images sans dimensions, composants asynchrones), la position « collée » peut sauter. Les utilisateurs appelleront ça « glitchy » ; vos metrics l’appelleront CLS.
7) Éléments sticky imbriqués et offsets concurrents
Deux en-têtes sticky dans des scrollers imbriqués peuvent se chevaucher. Le navigateur n’a pas tort ; c’est vous. Décidez qui possède quels pixels et coordonnez explicitement les offsets.
Blague 2/2 : Chaque fois que vous imbriquez un conteneur de scroll, un futur vous perd une heure et gagne une nouvelle opinion sur le « CSS simple ».
Playbook de diagnostic rapide
Ceci est la séquence « arrêter de deviner ». Exécutez-la dans l’ordre. Vous trouverez généralement le problème au troisième étape.
Première étape : confirmer le conteneur de défilement
- Identifiez quel élément défile réellement :
document, un div shell d’app, le corps d’un modal, ou un panneau. - Vérifiez les valeurs d’overflow des ancêtres le long du chemin de l’élément sticky.
- Si le conteneur de défilement n’est pas celui que vous attendez, corrigez cela avant de toucher au sticky.
Deuxième étape : confirmer les prérequis sticky
- L’élément sticky a
position: stickyet un seuil (topgénéralement). - L’élément sticky est à l’intérieur du conteneur de défilement attendu.
- L’élément sticky n’est pas contraint par un bloc contenant trop petit.
Troisième étape : vérifier les « killers » de sticky
- Un ancêtre a-t-il
overflow: hidden/clipsur l’axe sticky ? - Un ancêtre a-t-il
transform,filter,perspectiveou du containment qui change les systèmes de coordonnées ? - Le sizing flex/grid empêche le défilement (cherchez un
min-height: 0oumin-width: 0manquant). - Les z-index/contextes d’empilement cachent l’élément sticky derrière du contenu.
Quatrième étape : reproduire dans un DOM minimal
- Copiez l’élément sticky et ses ancêtres dans une page HTML minimale.
- Supprimez les styles jusqu’à ce que sticky recommence à fonctionner.
- La dernière suppression est la cause racine, pas « le CSS est cassé ».
Tâches pratiques : commandes, sorties et décisions
Les bugs sticky sont des bugs front-end, mais les déboguer bénéficie des habitudes de production : inspecter, capturer l’état, comparer les environnements et garder des traces. Voici des tâches concrètes à lancer localement ou en CI pour éviter le « ça marche sur ma machine » sticky.
Tâche 1 : Vérifier que le CSS déployé contient bien position: sticky
cr0x@server:~$ grep -R "position:\s*sticky" -n dist/assets | head
dist/assets/app.3d9c2.css:1842:.header{position:sticky;top:0;z-index:50}
dist/assets/app.3d9c2.css:9912:.toc{position:sticky;top:72px}
Ce que signifie la sortie : Votre bundle construit contient des règles sticky et les offsets attendus.
Décision : Si grep ne trouve rien, votre pipeline de build a supprimé ou réécrit la règle (config autoprefixer, extraction CSS, ou étape « critical CSS »). Corrigez les inputs de build avant de déboguer la mise en page.
Tâche 2 : Vérifier si une « optimisation » a ajouté massivement overflow: hidden
cr0x@server:~$ grep -R "overflow:\s*hidden" -n src styles | head -n 20
src/layout/Shell.css:12:.shell{overflow:hidden;height:100vh}
src/components/Card.css:4:.card{overflow:hidden;border-radius:12px}
styles/utilities.css:88:.clip{overflow:hidden}
Ce que signifie la sortie : Des wrappers de layout coupent l’overflow. C’est un suspect principal pour l’échec de sticky, surtout si appliqué au shell racine.
Décision : Si c’est sur le chemin de défilement principal, retirez-le ou déplacez-le vers le bas de l’arbre pour ne l’appliquer qu’aux éléments qui ont besoin de clipping (cards, images).
Tâche 3 : Confirmer que vous n’avez pas accidentellement déplacé le scroll dans un app shell
cr0x@server:~$ grep -R "overflow:\s*auto" -n src/layout | head
src/layout/Shell.css:16:.content{overflow:auto;min-height:0}
Ce que signifie la sortie : L’app scrolle à l’intérieur de .content, pas le document. Sticky s’ancrera à ce conteneur.
Décision : Placez les éléments sticky comme enfants de .content (s’ils doivent coller à l’intérieur), ou repensez pour que le document défile si vous voulez sticky au niveau de la fenêtre.
Tâche 4 : Détecter les hacks de transform qui changent le comportement contenant
cr0x@server:~$ grep -R "transform:\s*translateZ(0)" -n src styles | head
src/layout/Shell.css:21:.shell{transform:translateZ(0)}
Ce que signifie la sortie : Quelqu’un a utilisé le classique « GPU nudge ». Ça peut créer un nouveau bloc contenant/contexte d’empilement.
Décision : Enlevez-le sauf si vous pouvez prouver que ça corrige un vrai problème de perf. Remplacez par des transforms ciblés sur les éléments animés, pas sur tout le shell.
Tâche 5 : Vérifier les réglages de containment qui clipent ou isolent le painting
cr0x@server:~$ grep -R "contain:" -n src styles | head
src/layout/Shell.css:22:.shell{contain:paint}
src/components/Grid.css:7:.grid{contain:layout paint}
Ce que signifie la sortie : Le containment est utilisé. C’est une optimisation valide, mais ça peut aussi changer la façon dont les descendants sont positionnés et peints.
Décision : Si sticky est à l’intérieur d’un sous-arbre contenu et se comporte mal, retirez le containment de l’ancêtre ou restructurez pour que sticky soit en dehors de cette région isolée.
Tâche 6 : Prouver que l’élément sticky est couvert (audit z-index)
cr0x@server:~$ grep -R "z-index" -n src/components src/layout | head -n 25
src/layout/Header.css:9:.header{z-index:10}
src/components/Modal.css:3:.backdrop{z-index:100}
src/components/Popover.css:5:.popover{z-index:200}
src/components/Content.css:44:.content{position:relative;z-index:50}
Ce que signifie la sortie : Votre en-tête sticky a z-index: 10, mais du contenu ou des overlays peuvent être au-dessus.
Décision : Élevez le z-index de l’en-tête dans le contexte d’empilement approprié, ou enlevez le z-index concurrent des éléments qui ne devraient pas être superposés à un en-tête global.
Tâche 7 : Identifier les scrollers imbriqués sur une page en cours d’exécution avec Playwright (headless)
cr0x@server:~$ node -e "const { chromium } = require('playwright');(async()=>{const b=await chromium.launch();const p=await b.newPage();await p.goto('http://localhost:3000');const scrollers=await p.$$eval('*', els=>els.filter(e=>{const s=getComputedStyle(e);return (s.overflowY==='auto'||s.overflowY==='scroll') && e.scrollHeight>e.clientHeight;}).slice(0,15).map(e=>({tag:e.tagName,id:e.id,cls:e.className,scrollH:e.scrollHeight,clientH:e.clientHeight,overflowY:getComputedStyle(e).overflowY})));console.log(scrollers);await b.close();})();"
[
{ tag: 'DIV', id: 'app', cls: 'shell', scrollH: 3120, clientH: 900, overflowY: 'auto' },
{ tag: 'DIV', id: '', cls: 'modal-body', scrollH: 1410, clientH: 520, overflowY: 'auto' }
]
Ce que signifie la sortie : Le shell de l’app et un corps de modal sont des conteneurs de scroll. Sticky à l’intérieur de chacun se collera par rapport à eux.
Décision : Décidez quel scroller possède le sticky. Si vous voulez sticky au niveau viewport, arrêtez de scroller #app et laissez défiler le document.
Tâche 8 : Capturer les styles calculés pour un élément sticky lors d’un bug report
cr0x@server:~$ node -e "const { chromium } = require('playwright');(async()=>{const b=await chromium.launch();const p=await b.newPage();await p.goto('http://localhost:3000/docs');const sel='.toc';const s=await p.$eval(sel, el=>{const cs=getComputedStyle(el);return {position:cs.position,top:cs.top,overflow:cs.overflow,transform:cs.transform,zIndex:cs.zIndex};});console.log(s);await b.close();})();"
{ position: 'sticky', top: '72px', overflow: 'visible', transform: 'none', zIndex: 'auto' }
Ce que signifie la sortie : L’élément est sticky avec un offset top valide ; pas de transform sur lui-même.
Décision : Si cela semble correct, le problème est presque certainement dans un ancêtre (overflow/transform/contain) ou dans le contexte d’empilement (z-index « auto » plus un sibling avec contexte d’empilement).
Tâche 9 : Détecter automatiquement overflow/transform des ancêtres pour un sélecteur donné
cr0x@server:~$ node -e "const { chromium } = require('playwright');(async()=>{const b=await chromium.launch();const p=await b.newPage();await p.goto('http://localhost:3000/docs');const chain=await p.$eval('.toc', el=>{const out=[];let n=el.parentElement;let i=0;while(n&&i<12){const cs=getComputedStyle(n);out.push({tag:n.tagName,id:n.id,cls:n.className,overflowY:cs.overflowY,transform:cs.transform,contain:cs.contain});n=n.parentElement;i++;}return out;});console.log(chain);await b.close();})();"
[
{ tag: 'ASIDE', id: '', cls: 'toc-column', overflowY: 'visible', transform: 'none', contain: 'none' },
{ tag: 'DIV', id: '', cls: 'content', overflowY: 'auto', transform: 'none', contain: 'none' },
{ tag: 'DIV', id: 'app', cls: 'shell', overflowY: 'hidden', transform: 'translateZ(0)', contain: 'paint' }
]
Ce que signifie la sortie : Votre élément sticky est à l’intérieur de .content (conteneur de scroll), tandis que #app coupe l’overflow et applique transform/containment. C’est un champ miné pour sticky.
Décision : Retirez overflow:hidden de #app, abandonnez le hack transform, ou déplacez sticky en dehors de ce sous-arbre.
Tâche 10 : Confirmer que l’élément atteint réellement le seuil de sticky (test de scroll)
cr0x@server:~$ node -e "const { chromium } = require('playwright');(async()=>{const b=await chromium.launch();const p=await b.newPage();await p.goto('http://localhost:3000/docs');await p.evaluate(()=>{const sc=document.querySelector('.content');sc.scrollTop=0;});const before=await p.$eval('.toc', el=>el.getBoundingClientRect().top);await p.evaluate(()=>{const sc=document.querySelector('.content');sc.scrollTop=400;});const after=await p.$eval('.toc', el=>el.getBoundingClientRect().top);console.log({before,after});await b.close();})();"
{ before: 184, after: 72 }
Ce que signifie la sortie : Le top de l’élément est passé à 72px après le scroll : sticky s’est bien engagé dans ce scroller.
Décision : Si after continue de changer (ne se fige pas), sticky ne s’engage pas — cherchez des contraintes overflow/transform ou l’absence de top.
Tâche 11 : Capturer les déplacements de layout qui rendent sticky saccadé (proxy CLS via diffs de screenshots)
cr0x@server:~$ node -e "const { chromium } = require('playwright');(async()=>{const b=await chromium.launch();const p=await b.newPage({viewport:{width:1280,height:720}});await p.goto('http://localhost:3000/docs');await p.waitForTimeout(200);await p.screenshot({path:'s1.png',fullPage:false});await p.waitForTimeout(2000);await p.screenshot({path:'s2.png',fullPage:false});console.log('captured s1.png and s2.png');await b.close();})();"
captured s1.png and s2.png
Ce que signifie la sortie : Deux captures prises tôt et après le chargement probable de contenu asynchrone.
Décision : Si les positions de l’en-tête/TOC diffèrent entre les images, vous avez du contenu qui se charge tard ou des swaps de fontes qui déplacent la mise en page. Corrigez en réservant l’espace (tailles d’images explicites, stratégie font-display, skeletons).
Tâche 12 : Vérifier les règles d’overflow et de hauteur dans le CSS construit pour votre shell de layout
cr0x@server:~$ sed -n '1,120p' src/layout/Shell.css
.shell{
height:100vh;
overflow:hidden;
transform:translateZ(0);
contain:paint;
}
.content{
display:flex;
overflow:auto;
min-height:0;
}
Ce que signifie la sortie : Le shell est une boîte clipée, transformée et contenue pour le painting. Le contenu scrolle à l’intérieur.
Décision : Si vous voulez un en-tête sticky relatif à la fenêtre, cette architecture vous oppose. Soit laissez défiler le document, soit acceptez le sticky par conteneur et adaptez les offsets en conséquence.
Tâche 13 : Vérifier si une régression correspond à un changement récent (git blame avec intention)
cr0x@server:~$ git blame -L 1,40 src/layout/Shell.css
a81c9d12 (devA 2025-10-03 10:14:02 +0000 1) .shell{
a81c9d12 (devA 2025-10-03 10:14:02 +0000 2) height:100vh;
a81c9d12 (devA 2025-10-03 10:14:02 +0000 3) overflow:hidden;
a81c9d12 (devA 2025-10-03 10:14:02 +0000 4) transform:translateZ(0);
a81c9d12 (devA 2025-10-03 10:14:02 +0000 5) contain:paint;
a81c9d12 (devA 2025-10-03 10:14:02 +0000 6) }
Ce que signifie la sortie : Un seul commit a introduit plusieurs « tueurs de sticky » à la fois.
Décision : Revenir en arrière ou découper le changement. Si la motivation était la perf, benchmarkez correctement et réintroduisez uniquement ce que vous pouvez justifier.
Tâche 14 : Confirmer que l’élément sticky n’est pas accidentellement dans un ancêtre qui clippe (dump DOM)
cr0x@server:~$ node -e "const { JSDOM } = require('jsdom');const fs=require('fs');const html=fs.readFileSync('dist/index.html','utf8');const dom=new JSDOM(html);const el=dom.window.document.querySelector('.header');let n=el;let i=0;while(n&&i<8){console.log(i,n.tagName,n.id,n.className);n=n.parentElement;i++;}"
0 HEADER header
1 DIV app shell
2 BODY
3 HTML
Ce que signifie la sortie : L’en-tête est enfant du shell transformé/clipé.
Décision : Si le shell n’est pas censé être une couche contenant/clipante, restructurez : placez l’en-tête global en sibling du conteneur de scroll, ou retirez le clipping au niveau du shell.
Vous remarquez un schéma ? Nous n’« essayons pas du CSS au hasard ». Nous prouvons ce qui défile, ce qui contient, ce qui clippe et ce qui s’empile. Sticky fonctionne quand vous traitez le DOM comme un système.
Trois mini-histoires d’entreprise depuis les tranchées sticky
Mini-histoire 1 : L’incident causé par une mauvaise hypothèse
La société avait une console support client avec une barre de « statut de dossier » sticky en haut du panneau principal. Ça marchait pendant des mois. Puis une refonte est arrivée : la console a été déplacée dans un nouveau app shell avec une nav persistante à gauche et une « amélioration de perf » qui empêchait le document de défiler. Le scroll est passé dans .content.
Les agents support ont commencé à signaler que la barre de statut de dossier disparaissait parfois lors du défilement de dossiers longs. Certains ont pensé que c’était un bug de permissions car cela arrivait davantage sur des dossiers complexes (plus longs et nécessitant plus de scroll). La triage a étiqueté le bug « rendu intermittent ». Cette étiquette devrait être interdite.
Mauvaise hypothèse : « sticky colle à la fenêtre ». Ce n’était pas le cas. Il collait au conteneur de défilement le plus proche, qui était maintenant un div imbriqué. Mais la barre de statut n’était plus à l’intérieur de ce div — elle était un sibling. Elle ne s’engageait donc jamais. Sur certains écrans, ça paraissait « correct » parce que la barre restait visible en raison de la mise en page et de la hauteur de la fenêtre.
La correction n’a rien d’exotique : soit déplacer la barre dans le conteneur de scroll et la rendre sticky là, soit laisser défiler le document et la garder sticky par rapport à la fenêtre. Ils ont choisi la première option. L’incident a pris fin, et l’équipe a mis à jour ses directives de layout : celui qui crée le conteneur de scroll possède le comportement sticky. Point final.
Mini-histoire 2 : L’optimisation qui s’est retournée contre eux
Une autre équipe a construit un site de knowledge base avec un TOC sticky « Sur cette page ». C’était propre, rapide et stable — jusqu’à ce que quelqu’un lance un projet « réduire le coût de paint ». Ils ont saupoudré contain: paint sur des régions de layout et ajouté transform: translateZ(0) au conteneur racine pour « le promouvoir sur sa propre couche ».
Ils ont mesuré une amélioration sur un micro-benchmark : un test de scroll synthétique sur un appareil Android milieu de gamme. Puis ils ont déployé. Une semaine plus tard, des rapports : le TOC a cessé de coller sur Safari et l’en-tête rendait parfois sous le contenu pendant le scroll. Ça ressemblait à un problème de z-index mais n’était pas toujours corrigeable avec des changements de z-index.
La cause racine était un cocktail : le root transformé a créé un nouveau bloc contenant et contexte d’empilement ; le containment de peinture a modifié la façon dont les descendants étaient clipés et peints ; et l’élément sticky était maintenant dans un sous-arbre que le navigateur traitait différemment pendant le compositing de scroll. Les moteurs ont géré la combinaison différemment.
Ils ont rollbacké l’« optimisation », puis l’ont réintroduite sélectivement : seulement sur les composants qui animaient réellement, pas sur le root. Les perfs sont restées bonnes, sticky est redevenu ennuyeux, et l’équipe a retenu la leçon que l’exploitation répète : optimiser sans harnais de correction, c’est casser les choses efficacement.
Mini-histoire 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Un dashboard fintech avait plusieurs couches sticky : un en-tête global, une sous-navigation, et un en-tête par tableau sticky à l’intérieur d’une grille défilable. C’est le genre d’UI qui rend bien en maquette et devient une ferme à bugs en production.
Au lieu de « essayer et voir », l’équipe a écrit un petit ensemble de tests de layout automatisés avec Playwright. Pas de snapshots visuels fancy — juste des assertions géométriques : identification du conteneur de scroll, vérifications de boundingClientRect après scroll, et un sanity check que le top de l’en-tête sticky égale l’offset attendu.
Des mois plus tard, une refactorisation a changé un wrapper en overflow: hidden pour corriger un souci d’angles arrondis. Les tests sticky ont échoué en CI immédiatement. Le dev a vu l’échec, a déplacé le clipping sur un élément enfant, et a préservé le comportement d’overflow du conteneur de scroll. Aucun client n’a jamais remarqué. Personne n’a écrit un long thread Slack sur Safari.
C’est la vérité peu sexy : la fiabilité sticky vient des garde-fous. Votre futur vous ne se souviendra pas pourquoi min-height: 0 était important dans cet enfant flex. Les tests le feront.
Erreurs courantes : symptôme → cause racine → correction
Sticky n’active jamais (il défile comme d’habitude)
Symptôme : L’élément se comporte comme position: relative ; il ne se clipse jamais en haut.
Cause racine : Absence de top/bottom, ou l’élément sticky n’est pas dans la boîte de défilement que vous croyez.
Correction : Ajoutez top: 0 (ou l’offset correct). Confirmez le scroller avec un audit de chaîne d’ancêtres. Déplacez sticky à l’intérieur du conteneur de scroll.
Sticky fonctionne, mais s’arrête trop tôt
Symptôme : L’élément colle brièvement, puis repart avant la fin de la section.
Cause racine : Le bloc contenant (souvent le parent) est plus court que le contenu scrollable, donc sticky est contraint et atteint la fin du conteneur.
Correction : Faites en sorte que l’élément frontière destiné englobe toute la région où sticky doit opérer. Évitez d’appliquer sticky à un élément dont le parent a une hauteur limitée de façon inattendue.
Sticky fonctionne, mais est caché sous du contenu
Symptôme : Il est « là », mais le contenu défile par-dessus.
Cause racine : Problèmes de contexte d’empilement et z-index (souvent causés par des transforms ou des éléments positionnés avec z-index).
Correction : Donnez à l’élément sticky un z-index non-auto dans le contexte d’empilement correct. Retirez les z-index inutiles des siblings. Évitez les transforms au niveau root qui créent des contextes d’empilement.
Sticky scintille ou tremble pendant le scroll
Symptôme : Au scroll, l’élément sticky tremble, repainte ou saute d’un pixel.
Cause racine : Arrondis sous-pixels + changements de compositing + contenu dynamique ; parfois aggravé par le chargement des polices ou backdrop-filter.
Correction : Réduisez la complexité de compositing : évitez de combiner sticky avec des effets lourds sur les ancêtres. Réservez l’espace pour les fontes/images pour éviter les déplacements tardifs de layout.
Sticky casse seulement dans un modal
Symptôme : Ça marche sur la page, échoue dans les dialogues modaux.
Cause racine : Le corps du modal est le scroller ; l’élément sticky est en dehors, ou un ancêtre clippe l’overflow.
Correction : Placez les en-têtes sticky à l’intérieur du corps modal défilable. Faites du corps modal le conteneur de scroll explicitement.
La barre latérale sticky recouvre le footer ou une autre section
Symptôme : La barre latérale continue de coller et couvre du contenu en bas.
Cause racine : La bordure sticky est trop grande (par ex. le conteneur de la barre latérale s’étend sur toute la page), ou les offsets ne tiennent pas compte du contenu bas.
Correction : Contraignez le bloc contenant de la barre latérale à la section où elle doit coller. Envisagez position: sticky; bottom: ... pour certains patterns, mais faites-le délibérément.
Sticky échoue seulement sur Safari / iOS
Symptôme : Ça marche sur Chromium/Firefox mais pas sur Safari.
Cause racine : Une combinaison de scroll imbriqué, transforms et effets ; ou le comportement des unités viewport qui crée des différences de layout modifiant les seuils.
Correction : Retirez transforms/containment des ancêtres, simplifiez les conteneurs de scroll et validez avec des vérifications géométriques automatisées sur WebKit. Préférez le container-sticky à l’intérieur d’un unique scroller overflow quand vous construisez des shells complexes.
Sticky à l’intérieur d’une colonne flex ne se comporte pas comme prévu
Symptôme : Il colle, mais pas au bon moment ; ou ne colle pas quand le contenu est court.
Cause racine : Le sizing flex et les defaults de min-size empêchent le scroll ou changent la hauteur du bloc contenant.
Correction : Ajoutez min-height: 0 aux enfants flex qui doivent scroller ; assurez-vous que le conteneur de l’élément sticky a la bonne hauteur et les bonnes règles d’overflow.
Listes de contrôle / plan étape par étape
Checklist : en-tête sticky qui doit coller à la fenêtre
- Faites défiler le document (évitez de scroller à l’intérieur de
#appsauf si nécessaire). - Assurez-vous qu’aucun ancêtre de l’en-tête sticky ne définit overflow sur l’axe vertical (
hidden,clip,auto), sauf si vous visez container-sticky. - Évitez les
transform,filter,containau niveau root sur les wrappers contenant l’en-tête. - Définissez
position: sticky; top: 0sur l’en-tête. - Donnez un background et un
z-indexréfléchi. - Vérifiez le comportement avec un scroll automatisé + assertion boundingClientRect.
Checklist : barre latérale sticky qui doit s’arrêter à la fin d’une section
- Créez un wrapper qui définit la bordure de la barre latérale (la colonne de section).
- Mettez un élément interne sticky à l’intérieur de ce wrapper.
- Réglez
toppour dégager l’en-tête global (ne devinez pas ; utilisez un token ou une variable CSS). - Assurez-vous que le wrapper n’a pas d’
overflowqui clippe sur l’axe sticky. - Si vous utilisez flex/grid, vérifiez que la hauteur du wrapper frontière correspond à la hauteur de section attendue.
- Testez avec du contenu long et court, et avec du contenu qui se charge tard.
Checklist : sticky dans un modal ou un panneau scroller
- Faites d’un élément le conteneur de scroll :
overflow: autosur le corps du modal. - Placez les en-têtes sticky à l’intérieur de ce conteneur de scroll.
- Assurez-vous que le seuil sticky prend en compte toute chrome modale fixe.
- Gardez les transforms/effets sur les ancêtres au minimum ; appliquez les effets aux siblings plutôt qu’aux parents si possible.
- Testez sur WebKit si vous ciblez iOS.
Étape par étape : quand sticky est cassé et que vous devez le réparer aujourd’hui
- Trouvez le conteneur de défilement en utilisant un scan automatisé (comme la détection de scrollers Playwright). Décidez si c’est correct.
- Imprimez la chaîne d’ancêtres pour l’élément sticky et cherchez overflow, transform, contain, filter.
- Retirez temporairement les propriétés suspectes (dans devtools) en partant de l’ancêtre le plus proche vers l’extérieur.
- Corrigez l’architecture : arrêtez d’imbriquer des scrollers, ou adoptez container-sticky et déplacez le nœud sticky à l’intérieur du scroller.
- Verrouillez-le avec un test géométrique pour que le même bug ne revienne pas au prochain sprint déguisé.
FAQ
1) Pourquoi position: sticky cesse-t-il de fonctionner quand j’ajoute overflow: hidden à un parent ?
Parce que le clipping d’overflow change le contexte de défilement/bloc contenant dont dépend sticky. Dans bien des cas, il contraint sticky à la boîte de cet ancêtre ou empêche la relation de scroll nécessaire. Déplacez le clipping d’overflow vers un élément de plus bas niveau (comme la carte visuelle) plutôt que le wrapper de layout.
2) Sticky est-il relatif à la fenêtre ou à la page ?
Ni l’un ni l’autre par défaut. Sticky est relatif au conteneur de défilement pertinent le plus proche pour cet axe. Si le document défile et qu’aucun ancêtre ne crée un contexte de scrolling/clip, il apparaîtra comme relatif à la fenêtre.
3) Quand dois-je utiliser position: fixed plutôt que sticky ?
Utilisez fixed quand l’élément doit rester épinglé à la fenêtre quelle que soit sa position dans le DOM, et que vous acceptez de gérer manuellement les offsets de mise en page (padding/marges). Utilisez sticky quand l’élément doit seulement coller à l’intérieur d’une section ou d’une bordure de conteneur.
4) Pourquoi mon en-tête sticky recouvre le contenu ?
Sticky ne réserve pas d’espace comme le ferait un en-tête statique ; il change de position pendant le scroll. Donnez au contenu un padding/margin-top égal à la hauteur de l’en-tête si celui-ci recouvre. Assurez-vous aussi que l’en-tête a un background et un empilement cohérents.
5) Puis-je avoir deux en-têtes sticky (global + local) ?
Oui, mais vous devez coordonner les offsets. Le top de l’élément sticky local doit tenir compte de la hauteur du sticky global. S’ils sont dans des conteneurs de scroll différents, repensez la mise en page — le sticky imbriqué sur des scrollers imbriqués est le lieu où les bugs se multiplient.
6) Pourquoi sticky se comporte-t-il différemment dans Safari ?
Safari est plus sensible aux combinaisons de scroll imbriqué, transforms et effets qui créent de nouveaux blocs contenant ou couches de compositing. La correction pratique est architecturale : réduisez les scrollers imbriqués et évitez les transforms/containment sur les ancêtres de sticky.
7) z-index fonctionne-t-il sur les éléments sticky ?
Oui, mais seulement à l’intérieur du contexte d’empilement pertinent. Si un ancêtre crée un nouveau contexte d’empilement (souvent via transform ou un élément positionné avec z-index), vous pouvez augmenter le z-index autant que vous voulez et perdre quand même face à un sibling dans un autre contexte. Corrigez d’abord les contextes d’empilement, puis le z-index.
8) Pourquoi sticky « ne colle pas » dans un layout flex ?
Souvent parce que l’élément qui devrait scroller ne scrolle pas réellement : les items flex ont des defaults de min-size qui peuvent empêcher le rétrécissement, donc le conteneur de scroll n’obtient jamais de barre de défilement. Ajoutez min-height: 0 au bon enfant flex et vérifiez les réglages d’overflow.
9) Le sticky basé sur JavaScript (listeners de scroll) est-il jamais justifié ?
Parfois — quand vous avez besoin d’un comportement que le CSS ne peut pas exprimer (détection complexe de collision, snapping, frontières dynamiques). Mais traitez-le comme une fonctionnalité perf avec budget : utilisez IntersectionObserver si possible, évitez les lectures/écritures layout par frame, et testez sur des appareils bas de gamme.
10) Comment éviter le jitter sticky causé par du contenu dynamique ?
Réservez l’espace : dimensions d’images explicites, stratégie de chargement de fontes stable, et évitez d’injecter du DOM au-dessus des régions sticky après le rendu initial. Si le contenu doit évoluer, envisagez d’animer les changements de hauteur soigneusement et vérifiez le top calculé de l’élément sticky pendant les tests de scroll.
Prochaines étapes que vous pouvez réellement faire
Sticky fonctionne quand vous cessez de le traiter comme de la magie et que vous commencez à le traiter comme un système contraint : conteneurs de défilement, blocs contenant et contextes d’empilement. Décidez où vit le défilement, placez sticky dans ce contexte de scroll, et retirez les styles « aidants » des wrappers qui changent discrètement les systèmes de coordonnées.
Faites ceci ensuite, dans l’ordre :
- Choisissez un scroller principal pour chaque expérience (page, modal, panneau). Évitez l’imbrication sauf si vous avez une bonne raison.
- Exécutez un audit de chaîne d’ancêtres pour chaque élément sticky (overflow, transform, contain, filter, z-index).
- Standardisez les offsets sticky en utilisant des variables CSS (la hauteur de l’en-tête ne devrait pas être une supposition).
- Ajoutez un test géométrique automatisé par composant sticky : scroller, mesurer boundingClientRect, affirmer le seuil sticky.
Si vous voulez du sticky sans douleur, construisez-le comme vous construisez des systèmes en production : rendez l’environnement prévisible, mesurez ce que fait le navigateur, et retirez les pièges avant qu’ils ne vous enlèvent votre week-end.