Hoe scroll-getriggerde animaties CLS veroorzaken
Je hide-on-scroll-header laat je CLS mogelijk ongemerkt falen. Dit is waarom, en zo los je het op.
Word ik gevraagd voor een audit van de Cumulative Layout Shift, dan begin ik natuurlijk met shifts tijdens de laadfase. Ik check op afbeeldingen zonder afmetingen, geïnjecteerde advertenties en font swaps (FOUT).
Maar niet alle CLS ontstaat tijdens het laden van de pagina. Soms wordt het getriggerd door interacties zoals scrollen. Die zijn lastig te vangen: Lighthouse scrolt niet en je moet precies weten waar je moet kijken.
Laatst beoordeeld door Arjen Karel in maart 2026
Bij een recente audit voegde ik CoreDash RUM-data toe en zag een verdacht patroon. Een #header-element leek veel meer layout shift te veroorzaken dan normaal.

Wat we hier zien blijkt een fixed header: hij verdwijnt zodra de gebruiker omlaag scrolt en verschijnt weer bij het omhoog scrollen. Dit patroon zie ik regelmatig. Doe je dit niet goed, dan veroorzaakt het een layout shift bij elke verandering van scrollrichting.
Waarom scroll-getriggerde shifts anders zijn
Normaal gesproken ontstaat CLS niet na een interactie. CLS meet onverwachte layout shifts, en na een interactie verwacht je juist dat de layout verandert. Daarom heeft de CLS-metric een automatische grace period van 500 ms. Layout shifts binnen 500 milliseconden na een gebruikersinteractie tellen niet mee voor de CLS-score. De browser regelt dit door bij deze shifts een hadRecentInput-flag te zetten en te controleren.
Het probleem is dat dit alleen geldt voor discrete events zoals klikken, tikken en toetsaanslagen. Volgens de Layout Instability-specificatie is scroll expliciet geen uitsluitende input. Slepen en pinch-zoom-gebaren evenmin. Alleen discrete events krijgen de grace period van 500 ms.
Dit betekent dat elke layout shift die door een scroll-event wordt getriggerd, meetelt voor je CLS-score.
Volgens de Web Almanac van 2025 gebruikt 40% van de mobiele pagina's nog altijd non-composited animaties. Dit zijn animaties op properties zoals top, height of margin die op elk frame een herberekening van de layout triggeren. Vuren die af tijdens het scrollen, dan telt elk afzonderlijk frame mee voor je CLS.
Het probleem met de hide-on-scroll-header
Laten we beginnen met het probleem dat ik vandaag tegenkwam: de hide-on-scroll-header. Een fixed header gebruikt JavaScript om de zichtbaarheid tijdens het scrollen om te schakelen. De implementatie ziet er zo uit:
/* CSS */
#header {
position: fixed;
top: 0;
transition: top 0.3s ease;
}
/* JavaScript */
window.addEventListener('scroll', () => {
if (scrollingDown) {
header.style.top = '-80px'; // hide
} else {
header.style.top = '0px'; // show
}
}); De layout shift ontstaat door de combinatie van transition: top 0.3s ease en het aanpassen van header.style.top in JavaScript.
Dit is waarom: top is een layout-property. Verwerkt de browser een wijziging aan top, dan doorloopt hij de volledige rendering pipeline: Style, Layout, Paint, Composite. Op elk animation frame tijdens die transitie herberekent de browser de layout. Elke herberekening levert een nieuwe layout shift-entry op.
Bij elke verandering van scrollrichting blijft de CLS-teller oplopen zolang de gebruiker interactie heeft met de pagina.
Ik bouwde een demo die exact dit gedrag van een live site uit een audit nabootst en nam een video op. Na wat scrollen liep de totale CLS op tot boven de 0,1. Toegegeven: in de praktijk wordt er zelden zo intensief gescrold, maar het mechanisme verandert niet. En ik heb gezien dat deze layout shift sites net over het randje duwt, waardoor hun totale CLS faalt.
Welke CSS-properties veroorzaken layout shifts bij het animeren?
Wil een animatie CLS veroorzaken, dan moet hij Layout triggeren. De rendering pipeline van de browser kent na de style-berekening drie hoofdfasen: Layout, Paint en Composite. Properties die alleen de Composite-fase raken, veroorzaken nooit layout shifts. Lees het bijbehorende artikel voor meer details over hoe CSS-transities layout shifts veroorzaken.
Properties die layout triggeren (en CLS veroorzaken bij het animeren):
top, left, right, bottom, width, height, margin, padding, border-width, font-size
Al deze properties veranderen de geometrie of positie van een element. De browser moet de positie van het element (en mogelijk ook van andere elementen) herberekenen.
Properties die layout volledig overslaan (en veilig zijn voor animaties):
transform en opacity draaien op de compositor thread. Ze omzeilen zowel layout als paint. De browser verwerkt ze op de GPU (daarom werken deze animaties zelfs als de main thread bezig is). Omdat ze nooit een herberekening van de layout triggeren, leveren ze ook nooit layout shift-entries op.
Vervang layout-properties door transforms
De fix voor de hide-on-scroll-header is simpel. Vervang de top-animatie door transform: translateY().
/* CSS - BEFORE (causes CLS) */
#header {
position: fixed;
top: 0;
transition: top 0.3s ease;
}
/* CSS - AFTER (no CLS) */
#header {
position: fixed;
top: 0;
transition: transform 0.3s ease;
}
/* JavaScript - BEFORE */
header.style.top = '-80px';
/* JavaScript - AFTER */
header.style.transform = 'translateY(-80px)'; Dit geeft hetzelfde resultaat: de header schuift omhoog en verdwijnt. Maar transform: translateY() draait volledig op de compositor en veroorzaakt nooit een layout shift.
Hetzelfde principe geldt voor elke scroll-getriggerde animatie:
- Inschuivende panelen: gebruik
transform: translateX()in plaats vanleftofright - Uitklappende elementen: gebruik
transform: scaleY()in plaats vanheight - Vervagende elementen: gebruik
opacityin plaats vanvisibilityin combinatie met hoogteveranderingen
Andere scroll-getriggerde patronen die CLS veroorzaken
De hide-on-scroll-header zie ik regelmatig langskomen, maar er is meer. Alle JavaScript die een layout-property aanpast als reactie op scroll-events veroorzaakt een layout shift. Bijvoorbeeld:
Parallax-effecten met top of margin-top. Vervang dit door transform: translateY() of gebruik de CSS-aanpak met perspective en translateZ voor parallax in pure CSS.
Sticky navigatie die van hoogte verandert bij het scrollen. Een navigatiebalk die krimpt van 80px naar 50px als de gebruiker voorbij een bepaalde drempel scrolt. Is die hoogteverandering geanimeerd met transition: height, vervang dit dan door transform: scaleY().
Scroll-gekoppelde voortgangsbalken op basis van width. Een leesindicator die van links naar rechts groeit door width te animeren. Gebruik in plaats daarvan transform: scaleX() met transform-origin: left.
Modern alternatief: CSS Scroll-Driven Animations. Met de animation-timeline: scroll()-API stuur je animaties aan op basis van scrollvoortgang, helemaal zonder JavaScript. Doordat deze animaties standaard transform en opacity gebruiken en op de compositor draaien, veroorzaken ze nooit layout shifts. De browserondersteuning omvat Chrome 115+ en Edge 115+; voor Safari is de ondersteuning nog in ontwikkeling. Voor effecten gekoppeld aan scrollen, zoals parallax, voortgangsbalken en reveal-animaties, is dit de schoonste oplossing. Wil je JavaScript-scrolling volledig dumpen, dan is de scroll-driven animations-API de juiste weg.
Waarom Lighthouse dit niet ziet
Lighthouse draait een gesimuleerde page load. Het scrolt niet over de pagina. Het voert na het laden geen interacties uit. Daardoor zijn scroll-getriggerde layout shifts in labtests volledig onzichtbaar.
De enige manier om deze shifts te vangen is met Real User Monitoring. Field data legt de complete levenscyclus van de pagina vast, inclusief elke layout shift die optreedt terwijl de bezoeker scrolt, klikt en navigeert. Is je CrUX CLS-score slechter dan je Lighthouse CLS-score, dan zijn scroll-getriggerde animaties een van de eerste zaken om te onderzoeken. CoreDash meet CLS van echte bezoekers, inclusief elke shift veroorzaakt door scrollen. Zo zie je precies welke elementen hiervoor verantwoordelijk zijn.
Zo vind je scroll-getriggerde layout shifts
Open Chrome DevTools. Ga naar het Performance-paneel. Start de opname (het ronde icoon) en scrol een paar keer op en neer over de pagina. Stop de opname. Zoek naar paarse ruiten in de Layout Shifts-track. Zie je een cluster van shifts die samenvallen met je scrollactiviteit, dan heb je een scroll-getriggerd CLS-probleem. Je kunt ook de Core Web Vitals Visualizer Chrome-extensie gebruiken om CLS realtime te zien terwijl je scrolt.

De vuistregel
Beweegt het bij het scrollen, animeer het met transform. Vervaagt het bij het scrollen, animeer het met opacity. Deze properties zijn compositor-only.
Voor JavaScript: check je scroll event handlers, IntersectionObserver-callbacks en alle JavaScript die styles van elementen aanpast op basis van de scrollpositie. Gebruikt die code top, left, margin, width, height of padding, vervang dit dan door de equivalente transform.

Een snellere site nodig?
Ik ben Arjen en ik maak al 17 jaar trage sites snel. Loop je tegen iets aan dat je zelf niet opgelost krijgt? Stuur me je URL, dan praten we even.
Stuur me je URL
Waarom werkt onze fix niet?
Soms serveert het CDN nog de oude HTML, of de fix zit in het desktop-template terwijl mobiel een ander template gebruikt. Stuur me je URL en het probleem, dan stuur ik je een paar momenten. Ik bekijk eerst je field data, daarna besteden we een uur aan je site, en binnen een dag heb je mijn notities op schrift. €350 excl. btw.
Stuur me je URLGeen developer om de fix te bouwen? Ik doe het werk zelf, op een kopie van je site.