What Causes Cumulative Layout Shift (CLS) and How to Fix It
Why your page layout jumps around, what causes high CLS scores, and how to fix every common source of layout shift.
What Causes Cumulative Layout Shift (CLS) and How to Fix It
You click a button. The page shifts. You click something else instead. CLS measures exactly this: how much the visible content moves around during its lifetime.
The metric shows up in Lighthouse, PageSpeed Insights, and Chrome DevTools:
Cumulative Layout Shift: 0.42
A good score is under 0.1. Anything above 0.25 is poor. If you’re seeing numbers like 0.42, your users are watching content jump around the page every time something loads.
CLS is one of Google’s three Core Web Vitals. It affects your search ranking, but more importantly, it affects whether your users can actually click the thing they’re aiming for.
What causes layout shift
Layout shift happens when a visible element changes position after the browser has already painted it. The browser renders what it knows, then something new arrives (an image, a font, an ad, a dynamically injected element) and pushes everything else around.
Every shift gets a score based on how much of the viewport moved and how far it moved. CLS is the sum of all unexpected shifts during the page’s lifetime.
Common causes
1. Images and videos without dimensions
When the browser encounters an <img> tag with no width and height attributes, it allocates zero space for it. Once the image downloads, the browser recalculates layout and shoves everything below it down the page.
<!-- Bad: no dimensions, causes shift -->
<img src="/hero.jpg" alt="Hero image" />
<!-- Good: explicit dimensions -->
<img src="/hero.jpg" alt="Hero image" width="1200" height="630" />
Modern CSS handles this with the aspect-ratio property, which works even with responsive images:
img {
max-width: 100%;
height: auto;
aspect-ratio: attr(width) / attr(height);
}
Or set it directly:
.hero-image {
width: 100%;
aspect-ratio: 16 / 9;
object-fit: cover;
}
The width and height attributes don’t force a fixed pixel size. Browsers use them to calculate the aspect ratio and reserve the correct amount of vertical space before the image loads.
2. Web fonts causing text reflow
When a custom font loads, the browser swaps from the fallback font to the web font. If the two fonts have different metrics (character width, line height, letter spacing), every line of text reflows. On a text-heavy page, this shift is massive.
Font swap detected: "Inter" loaded, replacing system fallback.
Layout shift score: 0.18
Fix with font-display and size-adjust:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
The size-adjust and override properties match the fallback font’s metrics to the web font, so when the swap happens, the text doesn’t reflow.
Preloading fonts also helps reduce the swap window:
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin />
3. Dynamically injected content
Banners, cookie notices, newsletter popups, A/B test variations. Anything that gets injected into the DOM after initial paint pushes content around.
// This causes layout shift if inserted at the top of the page
const banner = document.createElement('div')
banner.textContent = 'Free shipping today!'
document.body.prepend(banner)
Fix: Reserve space for dynamic content before it loads.
.promo-banner-slot {
min-height: 48px;
}
If the content might not appear, use CSS contain to isolate its layout impact:
.dynamic-slot {
contain: layout;
min-height: 0;
}
For content that renders above the fold on page load, server-render it instead of injecting it client-side.
4. Ads and embeds without reserved space
Third-party ad slots are the single biggest cause of CLS on most publisher sites. The ad network returns a creative with an unknown height, and the page reflowes around it.
<!-- Bad: ad slot with no dimensions -->
<div id="ad-slot"></div>
<!-- Good: reserved space matching the most common ad size -->
<div id="ad-slot" style="min-height: 250px; min-width: 300px;">
<!-- Ad loads here -->
</div>
If the ad doesn’t fill, you have empty space. That’s the tradeoff. Empty space is better than a layout shift that makes users misclick.
For embeds (YouTube, Twitter, iframes), always specify dimensions:
<iframe
src="https://www.youtube.com/embed/dQw4w9WgXcQ"
width="560"
height="315"
loading="lazy"
title="Video embed"
></iframe>
5. Late-loading CSS or JavaScript that changes layout
A stylesheet that loads after the page has painted can restyle elements and cause shifts. Same with JavaScript that adds classes, changes inline styles, or triggers layout recalculation.
// This causes a shift if it runs after paint
document.querySelector('.sidebar').style.width = '300px'
Fix: Critical CSS should be inlined or loaded in the <head> before the page paints. Non-critical CSS should not affect above-the-fold layout.
<head>
<style>
/* Critical CSS: layout-defining styles only */
.sidebar { width: 300px; }
.main { margin-left: 300px; }
</style>
<link rel="stylesheet" href="/styles/full.css" media="print" onload="this.media='all'" />
</head>
Measuring CLS
In Chrome DevTools, open the Performance panel and record a page load. Layout shifts appear as red markers in the Experience row. Click one to see which elements shifted and by how much.
The Performance Insights panel gives a direct CLS breakdown per element.
From JavaScript, the PerformanceObserver API reports layout shifts in real time:
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log('Layout shift:', entry.value, entry.sources)
}
}
})
observer.observe({ type: 'layout-shift', buffered: true })
The hadRecentInput check filters out shifts caused by user interaction (scrolling, clicking, typing), which don’t count toward CLS.
Prevention checklist
Set explicit dimensions on every <img>, <video>, and <iframe>. Preload fonts and use font-display: swap with metric overrides. Reserve space for ads, banners, and any content injected after initial paint. Inline critical CSS in the <head>. Avoid inserting content above existing content after the page is visible.
CLS is the easiest Core Web Vital to fix because the causes are predictable and the solutions are mostly CSS. The hard part is finding all the shift sources on a complex page.
Hushbug flags layout shift sources and performance issues automatically while you browse. Coming soon to the Chrome Web Store.