# How to fix cumulative layout shift, cause by cause

Your page jumps, users miss clicks, and your Core Web Vitals report shows CLS issues. Here is how CLS is measured, how to find the culprit, and the exact fix for each cause.

Updated 2026-09-14 · Source: https://porteur.ai/guides/how-to-fix-cumulative-layout-shift

## What a good CLS looks like and how it is measured

Aim for a CLS at or below 0.1. Above 0.25 is poor. That is the scoring as of 2026.

CLS is measured over the page’s lifetime in session windows. A window of shifts stays open while shifts keep happening, and it closes after one second without a shift or five seconds total. Your page’s score is the largest window. Shifts within 500 ms of a user input do not count.

Google’s field assessment uses the 75th percentile of real Chrome users over 28 days, per URL or per origin. That is the figure that shows in Search Console and the top of PageSpeed Insights when there is enough traffic. iOS is not in that dataset.

> Field data wins. Lighthouse is one simulated load and can disagree with the field. Optimise to pass the Core Web Vitals assessment in field data.

## Find what is shifting on your page

Start with PageSpeed Insights on the exact URL. Check the Mobile tab first. Look at the field section for the pass or fail, then the Lighthouse CLS figure in the lab section for hints.

1. **Reproduce the shift** Open your page in Chrome. Use an incognito window. Set the viewport to a common phone size. Reload. Note if a banner appears late or content jumps.
2. **Record a performance trace** Open DevTools, Performance panel. Tick Web Vitals. Click Start profiling and reload. Stop after the page is steady. In the summary, find Layout Shift entries.
3. **Show shift regions** Still in DevTools, enable Layout Shift Regions. Reload. Red boxes mark where the browser saw a shift. Note the element moving and the element causing it.
4. **Confirm in PageSpeed Insights** Run the URL. Compare the Lighthouse CLS audit to your trace. Use the screenshots and filmstrip to see when the jump happens.

Work page by page. Fix the highest traffic templates first: the home page, /pricing, and your top blog posts. That is where a small CLS improvement moves the most users into Good.

## Cause 1: media without dimensions

Images, videos, iframes and ads without width and height cause layout shifts. The browser reserves no space, the resource loads, then the layout changes.

- Add width and height attributes to every img. Use the intrinsic pixel size, the browser scales it down in CSS.
- If the size is fluid, add CSS aspect-ratio on the media box or wrapper.
- For videos and iframes, wrap in a box with aspect-ratio, then make the media fill the box.
- For ad slots, set a fixed min-height for each breakpoint. Collapse only when you have a known empty state.

```html
<!-- Image with dimensions -->
<img src="/assets/hero.avif" alt="Dashboard" width="1600" height="1000" decoding="async">

/* Fluid wrapper with reserved ratio */
.media-16x9 { aspect-ratio: 16 / 9; width: 100%; }
.media-16x9 > img, .media-16x9 > iframe { width: 100%; height: 100%; object-fit: cover; }
```

Example. Before: /guides/getting-started loads a hero image without size, the H1 jumps when it decodes. After: width and height on the img and aspect-ratio on the wrapper, no jump.

## Cause 2: injected banners and sticky headers

Cookie notices, announcement bars and sticky headers that appear after render push content down. This is the classic layout shift banner problem on /pricing pages.

- Prefer an overlay that does not move content. Position it fixed, full width, and animate opacity, not height.
- If you must reserve space, set a min-height on the banner container from first paint. Toggle visibility inside the reserved box.
- Do not insert a new DOM node above the header after load. Render the slot server side, then fill it.
- For sticky headers, render at full height from the start, or reduce on scroll with transform: translateY, not top or height.

```css
/* Overlay consent banner that does not shift layout */
.consent-overlay { position: fixed; inset: 0; background: rgba(0,0,0,.6); display: grid; place-items: end center; }
.consent-box { width: min(720px, 90vw); margin: 24px; background: #fff; border-radius: 8px; transform: translateY(0); transition: opacity .2s ease; }
```

Example. Before: a cookie bar injects above the header and shifts the whole page. After: a fixed overlay with opacity transition, no content moves at all.

## Cause 3: web fonts swapping and metric changes

A web font that arrives late can swap in with different metrics. Text reflows, links move, and CLS rises. DebugBear has good deep dives, but here is the short fix list.

- Self host fonts as WOFF2. A third party host adds a DNS lookup, a connection and a TLS handshake.
- Preload the one font file used above the fold. Use rel=preload as=font with crossorigin.
- Use font-display: swap or optional so text shows with a fallback immediately.
- Match metrics so the fallback and the web font take the same space. Use size-adjust with ascent-override on the fallback pair.
- Or avoid the problem: a system font stack does not shift.

```css
<!-- Preload the above-the-fold font -->
<link rel="preload" href="/fonts/Inter-roman.woff2" as="font" type="font/woff2" crossorigin>

@font-face {
  font-family: 'Inter';
  src: url('/fonts/Inter-roman.woff2') format('woff2');
  font-display: swap;
  /* If needed, adjust metrics to match your fallback. Values are typical, not measured. */
}

body { font-family: system-ui, -apple-system, Segoe UI, Roboto, 'Inter', sans-serif; }
```

Example. Before: /blog/post uses a thin web font that loads late and shifts lines. After: preload, swap, and metric overrides, the text renders stable from first paint.

## Cause 4: content inserted late above the fold

Any component that appears after load and claims space above the fold will shift content. Think carousels initialising, price boxes rendering, or JavaScript inserting a hero line after fetch.

- Render the shell server side. Reserve space with min-height for dynamic blocks so the layout does not change when content arrives.
- Show skeletons inside fixed-height boxes for components that fetch data.
- Do not reorder DOM nodes after load. Append content inside containers that already exist.
- If you must hide something until ready, keep its box in the flow. Toggle visibility, not display, until it is hydrated.

Example. Before: /pricing fetches a quote slider and pushes plan cards down. After: the slider box has a fixed height from first paint, cards hold their place, content fades in.

## Cause 5: ads and third-party embeds

Ads, maps and video embeds often inject iframes with unknown sizes. The browser cannot reserve space. When the slot fills, the page jumps.

- Give every slot a fixed or responsive min-height by breakpoint. Never rely on auto height.
- For variable size ads, reserve the largest likely size for each viewport and collapse down inside it.
- Lazy load below-the-fold embeds. For YouTube and maps, use a clickable facade that swaps to the iframe on interaction.
- Load third-party scripts late. Defer or load on interaction. Keep the tag manager container lean so it does not block initial layout.
- If an embed must resize, constrain it in a wrapper with aspect-ratio so the change does not move neighbours.

```css
/* Responsive ad slot with reserved space */
.ad-slot { width: 100%; }
@media (max-width: 599px) { .ad-slot { min-height: 280px; } }
@media (min-width: 600px) and (max-width: 1023px) { .ad-slot { min-height: 250px; } }
@media (min-width: 1024px) { .ad-slot { min-height: 600px; } }
```

Example. Before: /blog/post drops in a YouTube iframe that pushes the first paragraph down. After: a poster image with a play button swaps to the iframe on click, no layout change before interaction.

## Cause 6: animations of layout properties

Animating top, left, width or height triggers layout and can move other elements. That counts as CLS. The fix is to animate transform and opacity, which do not affect layout.

```css
/* Bad: moves content and triggers layout */
.bad-banner { transition: height .2s ease; height: 0; }
.bad-banner.open { height: 80px; }

/* Good: overlay or transform so layout stays fixed */
.toast { position: fixed; left: 50%; bottom: 16px; transform: translate(-50%, 20px); opacity: 0; transition: transform .2s ease, opacity .2s ease; }
.toast.show { transform: translate(-50%, 0); opacity: 1; }
```

Example. Before: an expanding FAQ uses height transitions and nudges the content below. After: the answer overlays the flow or uses transform to reveal, neighbours do not move.

## Check the fix in the lab and in the field

First, confirm in the lab. Run Lighthouse in PageSpeed Insights or Chrome. CLS should fall, and the filmstrip should show no jumps after first content.

1. **Validate the template** Test a few representative URLs on Mobile. Aim to get lab CLS near zero on a clean profile.
2. **Ship and wait for field data** Field data updates over a 28 day window at the 75th percentile. Watch the URL in PageSpeed Insights and the Core Web Vitals report in Search Console.
3. **Check origin fallbacks** If a URL has low traffic, the field section may show origin-level data. Fix sitewide templates so the origin passes at 0.1 or below.
4. **Track regressions** Add a budget to CI to fail builds if CLS rises. Recheck after adding a new banner, a font, or an embed.

If Mobile passes and Desktop does not, prioritise Mobile. Google’s crawler is smartphone first, and the Mobile field data is what you see first in Search Console.

## Quick cross checks that often help

- Remove loading=lazy from any image visible at first paint. It does not cause CLS alone, but it can delay layout settling.
- Inline only the critical CSS. A late stylesheet can restyle the page and move content.
- Cut third-party scripts. Fewer scripts reduce late inserts and reflows.
- Keep the DOM lean. Large trees cost more to lay out and can make every change more disruptive.
- Fix LCP and TTFB issues in parallel. Faster delivery brings the page to a steady state sooner.

> Do not hide CLS by delaying visibility. Shifts that happen later still count, and users still feel them.

## Questions

### What causes a high CLS score on my site?

Most spikes come from media without dimensions, banners injected above content, late web fonts that change text metrics, content inserted above the fold after load, third-party embeds that resize, and animations of layout properties. Find which applies with DevTools Layout Shift regions, then apply the matching fix.

### How do I fix a layout shift banner without breaking consent rules?

Use an overlay that sits above the content and does not alter layout. If you must reserve space, render a fixed height container in the HTML at first paint and toggle its contents. Avoid inserting a new element above the header after load.

### How soon will Search Console show my improved CLS?

Field data rolls over a 28 day window. You may see early movement in a few days on high traffic pages, but you only pass the assessment when the 75th percentile over the window hits 0.1 or below. Use PageSpeed Insights to check per URL or the origin while you wait.

### Is a CLS of 0.12 OK, or do I need 0.1 exactly?

The Good threshold is 0.1. If you sit at 0.12 in field data, keep going. Shave a little more by reserving space for media and embeds and removing late injects. Template fixes compound across pages and can bring the origin under 0.1.

### Does Lighthouse CLS matter for rankings?

No. The Lighthouse score itself is not a ranking factor. Google uses field data from real Chrome users at the Good thresholds for the Core Web Vitals assessment. Use Lighthouse to debug and confirm fixes locally, but optimise to pass in field data.

### Do user interactions count toward CLS?

No. Shifts within 500 ms of a user input are excluded from CLS. If content moves without any input, it counts. Focus on stabilising the layout before any interaction happens.

## Read next

- [Core Web Vitals for a product site: the three numbers](https://porteur.ai/guides/core-web-vitals): The product founder’s guide to LCP, CLS and INP: what Google measures, why phones decide the pass, common causes, and how to test and fix.
- [How to read PageSpeed Insights: field data first, then the score](https://porteur.ai/guides/how-to-read-pagespeed-insights): Start with field data, not the score. Learn what Google uses, why scores vary, and how to turn a PageSpeed Insights run into a clear order of fixes.
- [Lighthouse vs PageSpeed Insights: lab and field](https://porteur.ai/compare/lighthouse-vs-pagespeed-insights): Use Lighthouse for repeatable lab checks, PageSpeed Insights for real-user field data. See why scores differ and which to trust for fixes and reporting.
- [Web fonts and page speed: load one face, and load it first](https://porteur.ai/guides/web-fonts-and-page-speed): Ship crisp type without slowing first paint or shifting layout. Choose one fast path, preload the face above the fold, and stop the swap from moving text.
- [Image optimisation for page speed: the hero image and everything below it](https://porteur.ai/guides/image-optimisation-for-page-speed): Fix the hero image first for LCP, then lazy-load the rest. Choose AVIF or WebP, set srcset and sizes, and add width and height to stop shift.
- [Third-party scripts: the page speed you gave away](https://porteur.ai/guides/third-party-scripts-and-page-speed): See what each third-party script costs and what to do: remove, load on interaction, defer, self-host, facade video and maps, and preconnect smartly.
- [Caching: what to cache, for how long, and what it does for crawling](https://porteur.ai/guides/caching-for-seo): Hashed assets cached for a year, HTML cached briefly and revalidated, stale-while-revalidate at the edge, and the caching mistakes that cost crawls.

Want a second opinion on your CLS fixes? Paste a URL and get a free check that reads your site, nearby searches and rivals in about thirty seconds, with three findings shown whole. Porteur connects to Search Console later if you want deeper field data. Free check: https://porteur.ai/
