Un sticky header est une barre de navigation qui reste en haut de l'écran pendant le défilement de la page. Ma variante préférée est la version « intelligente » : elle s'efface quand on défile vers le bas, puis réapparaît dès que l'utilisateur remonte. Ce comportement rend de l'espace vertical au contenu pour la lecture tout en gardant la navigation à portée de doigt. Dans ce guide, je construis d'abord la barre fixe en CSS pur, puis j'ajoute pas à pas le comportement de masquage au défilement qui détecte la direction.
L'approche la plus simple : position: sticky
Dans la plupart des cas, vous n'avez même pas besoin de JavaScript. position: sticky garde l'élément dans le flux normal mais le fixe dès qu'il atteint le seuil que vous définissez. Vous devez lui donner une valeur top ; sans elle, il ne se fixe jamais.
.site-header {
position: sticky;
top: 0;
z-index: 100;
background: #fff;
}
C'est tout. La barre démarre dans le flux de la page, se fixe quand elle touche le bord supérieur, et le reste du contenu défile en dessous. L'avantage de sticky sur fixed est que l'élément occupe d'abord un espace réel : il ne chevauche pas le contenu situé en dessous, donc vous n'avez pas besoin d'ajouter un padding-top de compensation.
Quand sticky ne fonctionne pas, c'est presque toujours pour l'une de deux raisons : soit vous n'avez pas fourni de seuil top, soit un élément parent possède overflow: hidden / auto / scroll. Le second désactive silencieusement sticky, car l'élément est désormais lié au contexte de défilement de ce conteneur.
Masquer et afficher selon la direction du défilement
Pour le comportement « intelligent » que nous voulons vraiment, il faut connaître la direction du défilement. La logique est simple : à chaque défilement, comparez la valeur scrollY actuelle avec la précédente. Si elle a augmenté, l'utilisateur descend ; si elle a diminué, il remonte. En descendant, on glisse la barre vers le haut pour la masquer ; en remontant, on la ramène.
.site-header {
position: sticky;
top: 0;
transition: transform 0.25s ease;
}
.site-header.cachee {
transform: translateY(-100%);
}
J'utilise transform: translateY(-100%) pour la performance : les modifications de transform et opacity sont gérées sur la couche de composition du navigateur, ce qui les anime à un fluide 60 fps sans déclencher de layout ni de paint. Chercher le même effet en modifiant top ou height est bien plus coûteux.
Écrire correctement l'écouteur de défilement
L'événement scroll se déclenche des dizaines de fois par seconde. Si vous lisez et écrivez le DOM à chaque déclenchement, la page saccade. La solution est de différer le travail à la prochaine image de rendu avec requestAnimationFrame et d'utiliser un drapeau pour l'empêcher de s'exécuter plus d'une fois par image.
const header = document.querySelector('.site-header');
let dernierScroll = 0;
let enAttente = false;
function maj() {
const y = window.scrollY;
// Toujours afficher tout en haut
if (y <= 0) {
header.classList.remove('cachee');
} else if (y > dernierScroll && y > 80) {
// On descend au-delà du seuil -> masquer
header.classList.add('cachee');
} else if (y < dernierScroll) {
// On remonte -> afficher
header.classList.remove('cachee');
}
dernierScroll = y;
enAttente = false;
}
window.addEventListener('scroll', () => {
if (!enAttente) {
window.requestAnimationFrame(maj);
enAttente = true;
}
}, { passive: true });
Quelques petits détails rendent ce code utilisable :
{ passive: true }: indique au navigateur « cet écouteur n'appellera paspreventDefault», il peut donc défiler plus fluidement sans blocage.- Le seuil
y > 80: évite que la barre scintille dans les premiers pixels en haut de la page. Le masquage ne commence pas avant que l'utilisateur ait un peu descendu. - Toujours afficher en haut : la vérification
y <= 0garantit que l'en-tête est visible quand vous revenez en haut de la page.
Changer l'apparence une fois fixée
Autre demande fréquente : l'en-tête est transparent/grand en haut de la page, puis prend un fond et rétrécit une fois défilé. On le fait en basculant une classe selon que la barre est « fixée » ou non. Une façon propre de le détecter sans interrogation continue est IntersectionObserver : placez un élément sentinelle de 1px juste au-dessus de l'en-tête ; quand il quitte l'écran, l'en-tête est fixé.
const sentinelle = document.querySelector('#header-sentinelle');
const io = new IntersectionObserver(([entree]) => {
header.classList.toggle('fixee', !entree.isIntersecting);
}, { threshold: 0 });
io.observe(sentinelle);
.site-header.fixee {
box-shadow: 0 2px 12px rgba(0,0,0,.08);
backdrop-filter: blur(8px);
}
L'avantage d'IntersectionObserver est qu'il fait cela sans appeler getBoundingClientRect() à chaque événement scroll, laissant le navigateur planifier le travail lui-même — ce qui est bien plus efficace.
Accessibilité et mobile
Les sticky headers créent des problèmes d'accessibilité quand ils sont mal construits. Ne négligez pas ceci :
- Liens d'ancrage : cliquer sur un lien interne
#sectionpeut placer la cible sous l'en-tête fixe. Ajouterscroll-margin-top: 80px;aux sections cibles résout ce problème. - Préférence de mouvement : désactivez la
transitiondans@media (prefers-reduced-motion: reduce), pour que la barre change instantanément pour les utilisateurs sensibles au mouvement. - Focus clavier : quand les liens de la barre reçoivent le focus via
Tabalors qu'elle est masquée, la barre doit redevenir visible. Comme nous masquons avectransformet nondisplay: none, cela fonctionne déjà. - Hauteur sur mobile : ne laissez pas l'en-tête manger trop d'espace vertical sur petits écrans ; le comportement masquer/afficher est ici particulièrement précieux car il rend tout l'écran pendant la lecture.
@media (prefers-reduced-motion: reduce) {
.site-header { transition: none; }
}
section[id] { scroll-margin-top: 80px; }
Questions fréquentes
Quelle est la différence entre position: sticky et fixed ?
fixed retire entièrement l'élément du flux normal et le fixe toujours au viewport ; il chevauche le contenu en dessous, donc il faut un padding-top manuel. sticky démarre dans le flux, ne se fixe qu'au seuil atteint, et se libère à la fin de son conteneur. Pour un en-tête pleine page, sticky est généralement plus propre.
L'en-tête scintille/saute pendant le défilement — pourquoi ?
Généralement deux raisons : soit vous n'avez pas limité l'événement scroll avec requestAnimationFrame, soit il n'y a pas de seuil masquer/afficher pour les premiers pixels en haut. Un seuil comme y > 80 plus le cadrage rAF supprime ce tremblement.
Peut-on faire masquer/afficher en CSS seul ?
Pas tout à fait. position: sticky gère la fixation en CSS, mais le comportement « masquer en descendant, afficher en remontant » nécessite de connaître la direction du défilement, et un petit bout de JavaScript reste aujourd'hui le moyen le plus fiable pour cela.
Votre barre d'en-tête ne se comporte pas comme elle le devrait ? Nous pouvons rendre les sticky headers, les menus déroulants et les problèmes de superposition (z-index) fluides et accessibles ensemble. Si vous avez un projet, contactez-moi.