Cumulative Layout Shift is the most fixable Core Web Vital — once you find the culprits. The challenge is finding them: CLS happens during the entire page lifetime, not just at load, and visual debugging is slow. The CLS Debugger records every layout shift event with the element responsible, the magnitude, and the moment it happened. This guide covers the eight common CLS sources, how the debugger surfaces each, and the canonical fixes.
CLS measures unexpected layout shifts during page load and interaction. Each shift is scored by impact fraction (% of viewport affected) × distance fraction (how far things moved). The cumulative score across the page session is your CLS. Google\'s benchmark: good < 0.1, needs improvement 0.1-0.25, poor > 0.25.
The single biggest CLS win: every <img> and <video> needs explicit width and height attributes — even when the image is responsive.
<!-- Bad: layout shifts when image loads -->
<img src="/hero.jpg" alt="Hero">
<!-- Good: browser reserves space immediately -->
<img src="/hero.jpg" alt="Hero" width="1600" height="900">
<!-- Good for responsive: aspect-ratio reserves the space -->
<img src="/hero.jpg" alt="Hero" width="1600" height="900"
style="width: 100%; height: auto;">
The width/height attributes provide the aspect ratio. Modern browsers use this to reserve the right amount of space before the image loads, eliminating shift.
Web fonts cause shifts in three ways: FOUT (text renders in fallback, then swaps to web font, shifts), FOIT (text invisible until web font arrives, then appears causing shift), or proper sizing mismatch between fallback and web font.
/* Use size-adjust to match fallback font metrics */
@font-face {
font-family: 'YourFont';
src: url('/fonts/your-font.woff2') format('woff2');
font-display: swap;
size-adjust: 105%;
ascent-override: 90%;
descent-override: 20%;
line-gap-override: 0%;
}
body {
font-family: 'YourFont', system-ui, sans-serif;
}
The size-adjust, ascent-override, etc. properties tune the fallback font\'s metrics to match the web font, eliminating shift when the swap happens. Chrome\'s font fallback calculator can compute the right values.
Ad slots that load dynamically need explicit dimensions reserved before the ad arrives:
<div class="ad-slot" style="min-height: 250px; width: 300px; display: flex; align-items: center; justify-content: center;"> <!-- Ad SDK injects here --> </div>
For ad slots that may serve multiple sizes, reserve the largest expected size — wasting some whitespace is better than shifting.
Cookie banners, support chat, sticky footers — anything that appears AFTER page load — should use position: fixed or position: sticky so they don\'t push existing content:
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
}
.support-chat {
position: fixed;
bottom: 20px;
right: 20px;
z-index: 1000;
}
Modern CSS\'s aspect-ratio property reserves space based on a ratio rather than fixed dimensions:
.responsive-video {
aspect-ratio: 16 / 9;
width: 100%;
}
.product-image {
aspect-ratio: 1 / 1;
width: 100%;
object-fit: cover;
}
Video players (YouTube iframe, Vimeo, native HTML5 with chrome) need their container sized before the player loads. Use aspect-ratio or explicit dimensions on the wrapper, not just the video element.
Custom elements that render after page load should declare expected dimensions in CSS so the browser reserves space:
my-product-card {
display: block;
min-height: 300px;
contain: layout;
}
my-product-card:not(:defined) {
height: 300px;
background: #f3f4f6;
}
Animations that change top, left, width, height, margin trigger layout for every frame and pollute CLS. Animate transform and opacity instead — they\'re composited and don\'t trigger layout:
/* Bad: causes layout shift */
.slide-in {
animation: slide 0.3s ease;
}
@keyframes slide {
from { left: -100px; }
to { left: 0; }
}
/* Good: composited, no layout shift */
.slide-in {
animation: slide 0.3s ease;
}
@keyframes slide {
from { transform: translateX(-100px); }
to { transform: translateX(0); }
}
Identify every layout shift source — images, fonts, ads, components — with line-level diagnostics.
Run CLS Debugger →