Skip to content
ExponaBLOG
TECHNICAL SEO FOR DEVELOPERS · DAY 042 / 100

Cumulative Layout Shift: a practical Technical SEO guide

Why your page jumps — and how to stop it.

Expona Admin9 min read

Cumulative Layout Shift: Why Your Page Jumps — and How to Stop It

You've built a fast site. Your Lighthouse score looks solid. Then a user clicks a link, starts reading an article, and halfway through the first paragraph the entire layout shifts down by half a screen. That jarring, unexpected movement is Cumulative Layout Shift, or CLS — one of Google's Core Web Vitals that measures visual stability as your page loads. A high CLS score doesn't just feel broken; it tanks your SEO rankings and drives users away.

I've debugged this issue on live e-commerce and SaaS platforms countless times. The frustration users feel when content moves under their cursor is real, and the technical causes are almost always predictable. In this article, I'll walk you through the four most common culprits and show you exactly how to fix them.

Understanding Cumulative Layout Shift

Before we dive into fixes, let's define what we're measuring. CLS is not about a single layout shift; it's about the cumulative sum of all unexpected layout changes during the page's lifetime. Google measures this from initial page load until the page becomes inactive (typically when the user starts interacting with it).

A good CLS score is below 0.1. Anything above 0.25 is poor. The metric matters because:

  • It's a Core Web Vital that directly influences search rankings.
  • It degrades user experience and increases bounce rates.
  • It often goes unnoticed on a developer's fast machine, but users on slower connections suffer most.

The shifts you care about are unexpected ones. If a user clicks a button and the layout changes, that's expected and doesn't count toward CLS. But if a banner appears or an image loads after the page is interactive, that's unexpected and counts against you.

Images Without Dimensions — Always Set Width and Height or an Aspect Ratio

This is the most common culprit I encounter. When you serve an image without explicit dimensions, the browser allocates zero vertical space for it initially. The rest of your content lays out. Then, when the image loads, the browser suddenly knows how tall it is and pushes everything else down.

I worked on a travel platform where product images were loaded via a CMS with no width or height attributes. The first image would load, and the entire article would shift down by 300 pixels. Multiply that by five images on the page, and users had a nightmare experience.

The fix is straightforward: always declare dimensions. Modern browsers support two approaches:

Approach 1: Explicit width and height

html
<img src="hero.jpg" alt="Product" width="800" height="600" />

This tells the browser to reserve exactly 800×600 pixels before the image loads. The image will scale to fit its container, but the space is reserved upfront.

Approach 2: Aspect ratio (modern and flexible)

html
<img src="hero.jpg" alt="Product" style="aspect-ratio: 16 / 9;" />

This is cleaner when you're serving images responsively. The browser calculates the height based on the width and the aspect ratio, so the space is still reserved before the image loads. This works in all modern browsers and degrades gracefully.

For responsive images with srcset, always include both approaches:

html
<img 
  src="hero-medium.jpg" 
  srcset="hero-small.jpg 600w, hero-medium.jpg 1200w, hero-large.jpg 1920w"
  alt="Product"
  width="1200"
  height="675"
/>

The width and height attributes don't constrain the image; they define the aspect ratio. The container width determines the actual size, but the space is reserved based on this ratio.

If you're using Next.js, the Image component handles this automatically. For other frameworks, make sure your CSS-in-JS solution or build process doesn't strip these attributes.

Ads and Embeds — Reserve Space With min-height Before They Load

Advertisements and third-party embeds (like YouTube videos, Twitter cards, or analytics scripts) are notorious for delayed loading. An ad network might take 2–3 seconds to serve an ad. During that time, your page looks fine. Then the ad loads and pushes your content down.

On a SaaS dashboard I worked on, we integrated a banner ad below the fold. It would load 3–4 seconds after the page was interactive, and users would see everything shift. The impact on our CLS was immediate and measurable.

The solution is to reserve space upfront using a container with a predefined min-height:

html
<div class="ad-container" style="min-height: 250px;">
  <!-- Ad code loads here -->
  <script async src="//pagead2.googlesyndication.com/..."></script>
</div>

By setting min-height: 250px, you guarantee the browser reserves at least 250 pixels vertically, even if the ad hasn't loaded yet. When the ad finally renders, it fits into that reserved space without shifting anything below it.

For embeds, the same principle applies. If you're embedding a YouTube video, don't just drop an <iframe> into the page. Wrap it in a container with an aspect ratio:

html
<div class="video-container" style="aspect-ratio: 16 / 9; min-height: 400px;">
  <iframe 
    src="https://www.youtube.com/embed/..."
    title="Video"
    allow="accelerometer; autoplay; clipboard-write; clipboard-read; encrypted-media; gyroscope; picture-in-picture"
    allowfullscreen
  ></iframe>
</div>

Combine aspect-ratio with min-height so the space is reserved before the iframe loads. The min-height serves as a fallback if the aspect ratio isn't supported.

For lazy-loaded embeds or ads that appear only when scrolled into view, the reserved space approach still works. The user won't see a shift because the space was already there.

Late-Loading Fonts — Fallback Metrics Adjustment Reduces Text Reflow

Custom web fonts are a silent killer for CLS. Your page starts rendering with a system font (Arial, sans-serif), and the text takes up a certain amount of vertical space. Then your custom font loads, and if the custom font has different metrics — wider, narrower, taller, shorter — the entire text block reflows and pushes everything below it down.

I've seen a CRM platform where a custom font loaded after 2 seconds. The body text, which initially rendered in Georgia, would shift horizontally and vertically when the custom font (with tighter letter-spacing) loaded. Users scrolling the page during this window would experience a noticeable jank.

There are two strategies to mitigate this:

Strategy 1: font-display: swap (fastest visual load)

html
<@font-face {
  font-family: 'CustomFont';
  src: url('custom-font.woff2') format('woff2');
  font-display: swap;
}>

With font-display: swap, the browser immediately renders text using a fallback font, then swaps to the custom font once it loads. This avoids the invisible text period but can cause a reflow when the swap happens. The key is to minimize the visual difference between your fallback and custom font.

Strategy 2: Adjust fallback metrics

Use the @font-face descriptor size-adjust to scale the fallback font so it closely matches your custom font's dimensions:

html
<@font-face {
  font-family: 'CustomFont';
  src: url('custom-font.woff2') format('woff2');
  font-display: swap;
  size-adjust: 95%;
}>

This tells the browser to scale the fallback font to 95% of its normal size, reducing the visual mismatch when the custom font swaps in. Modern browsers support this, and it's a game-changer for CLS.

Alternatively, preload your critical fonts to speed up their load time, so the swap happens faster and during a period when users aren't interacting with the page:

html
<link rel="preload" as="font" href="custom-font.woff2" type="font/woff2" crossorigin />

Preloading pushes the font download to the highest priority, often loading it before images and other resources. This reduces the window during which the fallback font is visible.

Cookie consent banners and promotional popups are a UX nightmare if they're not implemented carefully. I've worked on e-commerce sites where a cookie banner loads 1–2 seconds after the page is interactive, and it pushes the entire page content up. Users might be mid-scroll or mid-click, and suddenly the page shifts.

The fix is architectural: banners should overlay the content, not push it down. This means:

  • Use position: fixed or position: absolute to layer the banner on top of the page.
  • Apply z-index to ensure the banner sits above other content.
  • Never use margin or padding on the body or main content container to make room for the banner.

Here's the pattern:

html
<div class="cookie-banner" style="position: fixed; bottom: 0; left: 0; right: 0; z-index: 1000; background: white; padding: 16px; box-shadow: 0 -2px 8px rgba(0,0,0,0.1);">
  <p>We use cookies to improve your experience.</p>
  <button>Accept</button>
  <button>Decline</button>
</div>

The banner is fixed to the bottom of the viewport, so it doesn't participate in the document flow. The page content remains unchanged, and there's no CLS impact.

If you want the banner to be dismissible and reduce the content width when displayed (for mobile), use padding-bottom on the main content only after the banner is acknowledged. This ensures the shift happens after the page is interactive and the user has had time to dismiss the banner.

For promo banners that should appear at the top, follow the same pattern:

html
<div class="promo-banner" style="position: fixed; top: 0; left: 0; right: 0; z-index: 999; background: #f0f0f0; padding: 12px; text-align: center;">
  <p>🎉 Limited-time offer: 20% off. Use code SAVE20.</p>
  <button style="float: right; cursor: pointer; background: none; border: none; font-size: 18px;">✕</button>
</div>

The key is that this banner doesn't change the layout of the page body. Users can close it if it's in the way, but it doesn't shift anything.

Common Mistakes

  • Forgetting the aspect-ratio on lazy-loaded images: If you're using loading="lazy", you still need width, height, or aspect-ratio. Lazy loading doesn't negate the need to reserve space.
  • Setting width and height as CSS instead of HTML attributes: HTML attributes are better because they're available to the browser before CSS is parsed. Use both, but prioritize HTML attributes.
  • Assuming images with fixed containers won't shift: Even if your image container has a fixed width, the height can still shift if you don't declare the aspect ratio. Always declare both dimensions.
  • Placing ads in the document flow: If you use position: static or position: relative for an ad container without a reserved height, the ad will shift the page when it loads. Always reserve space or use fixed positioning.
  • Loading fonts without a fallback strategy: Don't just hope your custom font loads fast. Always use font-display: swap and consider size-adjust to minimize reflow.
  • Dismissing CLS as a minor issue: It's a Core Web Vital. Google explicitly ranks sites with good CLS higher. Ignoring it is ignoring your SEO.

Key Takeaways

  • Always declare image dimensions using width and height attributes or CSS aspect-ratio. This reserves space before the image loads and prevents CLS.
  • Reserve space for ads and embeds using min-height or aspect-ratio on a container. The space should be reserved before the third-party resource loads.
  • Use font-display: swap and size-adjust to minimize text reflow when custom fonts load. Preload critical fonts to speed up the swap.
  • Overlay banners using position: fixed instead of pushing content down. Never use margin or padding on the body to make room for a banner that loads late.
  • Test CLS on slow networks and slow devices. Use Chrome DevTools (Lighthouse) and WebPageTest to measure real-world impact.
  • Monitor CLS in production using Core Web Vitals tools like Google Analytics, CrUX, or specialized monitoring services. Your users' experience matters more than your local test results.
ShareXLinkedInReddit

Found this useful? Support the writing.

Buy me a coffee
Written by
Expona Admin
Site administrator

Comments

Loading comments…

Leave a comment

Comments are reviewed before they appear. Be kind and stay on topic.

Related articles