Een sticky header is een navigatiebalk die bovenaan het scherm blijft terwijl de pagina scrollt. Mijn favoriete variant is de "slimme" versie: hij schuift uit beeld als je naar beneden scrollt en verschijnt weer zodra de gebruiker omhoog begint te scrollen. Dit gedrag geeft verticale ruimte terug aan de inhoud om te lezen, terwijl de navigatie steeds binnen handbereik blijft. In deze gids bouw ik eerst de vastgezette balk met pure CSS, en voeg ik daarna stap voor stap het verberg-bij-scrollen-gedrag toe dat de scrollrichting detecteert.
De eenvoudigste aanpak: position: sticky
In de meeste gevallen heb je niet eens JavaScript nodig. position: sticky houdt het element in de normale flow maar zet het vast zodra het de drempel bereikt die je instelt. Je moet er een top-waarde aan geven; zonder die blijft het nooit plakken.
.site-header {
position: sticky;
top: 0;
z-index: 100;
background: #fff;
}
Dat is alles. De balk start in de paginaflow, zet zich vast wanneer hij de bovenrand raakt, en de rest van de inhoud scrollt eronder. Het voordeel van sticky boven fixed is dat het element eerst echte ruimte inneemt: het overlapt de inhoud eronder niet, dus je hoeft geen compenserende padding-top toe te voegen.
Als sticky niet werkt, is het bijna altijd een van twee redenen: je hebt geen top-drempel opgegeven, of een ouderelement heeft overflow: hidden / auto / scroll. Het tweede schakelt sticky stilletjes uit, omdat het element nu gebonden is aan de scrollcontext van die container.
Verbergen en tonen op basis van scrollrichting
Voor het "slimme" gedrag dat we echt willen, moeten we de scrollrichting kennen. De logica is eenvoudig: vergelijk bij elke scroll de huidige scrollY-waarde met de vorige. Is hij groter geworden, dan gaat de gebruiker omlaag; is hij kleiner, dan gaat hij omhoog. Bij omlaag schuiven we de balk naar boven om hem te verbergen; bij omhoog halen we hem terug.
.site-header {
position: sticky;
top: 0;
transition: transform 0.25s ease;
}
.site-header.verborgen {
transform: translateY(-100%);
}
Ik gebruik transform: translateY(-100%) voor de prestaties: wijzigingen in transform en opacity worden afgehandeld op de compositorlaag van de browser, wat betekent dat ze soepel op 60 fps animeren zonder layout of paint te triggeren. Hetzelfde effect najagen door top of height te wijzigen is veel duurder.
De scroll-listener correct schrijven
De scroll-gebeurtenis vuurt tientallen keren per seconde af. Als je de DOM bij elke afvuring leest en schrijft, hapert de pagina. De oplossing is het werk uit te stellen tot het volgende renderframe met requestAnimationFrame en een vlag te gebruiken om te voorkomen dat het meer dan eens per frame draait.
const header = document.querySelector('.site-header');
let laatsteScroll = 0;
let wachtend = false;
function update() {
const y = window.scrollY;
// Altijd tonen helemaal bovenaan
if (y <= 0) {
header.classList.remove('verborgen');
} else if (y > laatsteScroll && y > 80) {
// We scrollen omlaag voorbij de drempel -> verbergen
header.classList.add('verborgen');
} else if (y < laatsteScroll) {
// We scrollen omhoog -> tonen
header.classList.remove('verborgen');
}
laatsteScroll = y;
wachtend = false;
}
window.addEventListener('scroll', () => {
if (!wachtend) {
window.requestAnimationFrame(update);
wachtend = true;
}
}, { passive: true });
Een paar kleine details maken deze code bruikbaar:
{ passive: true }: vertelt de browser "deze listener roept geenpreventDefaultaan", zodat hij soepeler kan scrollen zonder te blokkeren.- De drempel
y > 80: voorkomt dat de balk flikkert in de eerste paar pixels bovenaan de pagina. Verbergen begint pas als de gebruiker een stukje naar beneden is gescrolld. - Altijd tonen bovenaan: de controle
y <= 0garandeert dat de header zichtbaar is wanneer je terugkeert naar de bovenkant van de pagina.
Het uiterlijk wijzigen zodra hij vastzit
Een ander veelgevraagd detail: de header is transparant/groot bovenaan de pagina, en krijgt daarna een achtergrond en krimpt zodra je scrollt. We doen dit door een klasse te wisselen op basis van of hij "vastzit". Een nette manier om dat te detecteren zonder steeds te pollen is IntersectionObserver: plaats een 1px schildwacht-element net boven de header, en wanneer dat het scherm verlaat, zit de header vast.
const schildwacht = document.querySelector('#header-schildwacht');
const io = new IntersectionObserver(([invoer]) => {
header.classList.toggle('vast', !invoer.isIntersecting);
}, { threshold: 0 });
io.observe(schildwacht);
.site-header.vast {
box-shadow: 0 2px 12px rgba(0,0,0,.08);
backdrop-filter: blur(8px);
}
Het mooie van IntersectionObserver is dat het dit doet zonder bij elke scroll-gebeurtenis getBoundingClientRect() aan te roepen, en de browser het werk zelf laat plannen — wat veel efficiënter is.
Toegankelijkheid en mobiel
Sticky headers veroorzaken toegankelijkheidsproblemen als ze slordig worden gebouwd. Vergeet deze niet:
- Ankerlinks: het klikken op een interne
#sectie-link kan het doel onder de vaste header laten landen.scroll-margin-top: 80px;aan de doelsecties toevoegen lost dit op. - Bewegingsvoorkeur: schakel de
transitionuit binnen@media (prefers-reduced-motion: reduce), zodat de balk direct verandert voor gebruikers die gevoelig zijn voor beweging. - Toetsenbordfocus: wanneer links in de balk via
Tabfocus krijgen terwijl hij verborgen is, moet de balk zichtbaar worden. Omdat we verbergen mettransformin plaats vandisplay: none, werkt dit al. - Hoogte op mobiel: laat de header niet te veel verticale ruimte opslokken op kleine schermen; het verberg/toon-gedrag is hier extra waardevol omdat het tijdens het lezen het hele scherm teruggeeft.
@media (prefers-reduced-motion: reduce) {
.site-header { transition: none; }
}
section[id] { scroll-margin-top: 80px; }
Veelgestelde vragen
Wat is het verschil tussen position: sticky en fixed?
fixed haalt het element volledig uit de normale flow en zet het altijd vast aan de viewport; het overlapt de inhoud eronder, dus je hebt een handmatige padding-top nodig. sticky begint in de flow, zet zich pas vast bij het bereiken van de drempel, en laat los wanneer zijn container eindigt. Voor een paginabrede header is sticky meestal netter.
De header flikkert/springt tijdens het scrollen — waarom?
Meestal twee redenen: ofwel heb je de scroll-gebeurtenis niet afgeknepen met requestAnimationFrame, ofwel is er geen verberg/toon-drempel voor de eerste pixels bovenaan. Een drempel als y > 80 plus rAF-framing verwijdert die trilling.
Kan verbergen/tonen met alleen CSS?
Niet helemaal. position: sticky regelt het vastzetten in CSS, maar het "verbergen bij omlaag, tonen bij omhoog"-gedrag vereist kennis van de scrollrichting, en een klein stukje JavaScript is daarvoor vandaag nog de betrouwbaarste manier.
Gedraagt je headerbalk zich niet zoals het hoort? We kunnen sticky headers, dropdownmenu's en laag- (z-index) problemen samen soepel en toegankelijk maken. Heb je een project? Neem contact met me op.