How to fix Cumulative Layout Shift, step by step

A practical recipe for a CLS above 0.1: tell load shifts from later ones, find what moved, and fix images, embeds, fonts, banners and late content.

IntermediateHow-to7 min readUpdated October 29, 2026

In this guide
  1. Step 1: is it a load shift or a later shift?
  2. Step 2: find what moved
  3. Step 3: apply the fix that matches
    1. Fix 1: give every image its size
    2. Fix 2: reserve the box for embeds and iframes
    3. Fix 3: stop banners from pushing the page
    4. Fix 4: make the fallback font match the web font
    5. Fix 5: keep a place for late content
    6. Fix 6: animate with transforms
  4. Step 4: check that it worked
  5. When a shift is not yours
  6. Check your own page
  7. Sources

Step 1: is it a load shift or a later shift?

Open the page in PageSpeed Insights and compare two numbers; what CLS measures explains how the score works. The real-user CLS is in the top section, and the lab CLS is in the performance metrics lower down.

Step 2: find what moved

For load shifts, use the report. In Lighthouse 13 the "Layout shift culprits" insight lists the biggest shifts, and for each it names causes it can recognize: an image without a size, a web font that loaded, an iframe that was injected, an animation. A separate check flags images without width and height. The element it lists is usually the one that moved, not the one that pushed it. If a paragraph is listed, look at what sits above it.

For anything else, watch it happen. In Chrome DevTools, record a page load in the Performance panel and look at the Experience track: each shift is marked, and selecting one shows the score and the rectangles of the elements that moved. For shifts that happen while scrolling, record while you scroll and use the page the way a visitor does. DevTools can also highlight layout shift regions while you browse.

For shifts you cannot reproduce, measure real visitors. The web-vitals library has an attribution build that reports the element that shifted the most (largestShiftTarget), when it shifted and by how much, and the load state at that moment. Send it to your analytics for a few weeks and the same few elements will show up again and again.

Step 3: apply the fix that matches

What movedUsual causeFix
Text below an imageImage without dimensionsFix 1
Text below a video or embedEmbed with no reserved heightFix 2
The whole page after a short delayBanner, bar or pop-up pushed itFix 3
Text lines, buttons, the headerWeb font swapFix 4
A block below a widgetLate content with no placeholderFix 5
Something that slides or jumpsAnimation with layout propertiesFix 6

Fix 1: give every image its size

Add width and height attributes that match the image file's own dimensions. The browser turns them into an aspect ratio before the file arrives, and your CSS can still make the image fluid:

<img src="/img/team.jpg" width="800" height="600" alt="The team at the front desk">
img { max-width: 100%; height: auto; }

The height: auto is what keeps the shape correct when CSS changes the width. The image optimization guide covers sizes and formats. For images in a container that does not have intrinsic proportions, such as a hero area built from a background image, give the container an aspect-ratio or a min-height. If your CMS or page builder outputs images without sizes, check its image settings, or its filters, before editing every page by hand: WordPress, for example, adds width and height to the images it generates in the editor, and the missing ones are usually from the theme or from plugins.

Fix 2: reserve the box for embeds and iframes

A video, map or social post is an iframe that has no height until it loads. Put it in a box with a fixed shape:

.embed { aspect-ratio: 16 / 9; width: 100%; }
.embed iframe { width: 100%; height: 100%; border: 0; }

For widgets with a content-dependent height, such as a review feed, set a min-height at the most common size. Where you know the number of items, reserve the space for all of them.

Fix 3: stop banners from pushing the page

A cookie notice, promotion bar or "join our newsletter" strip that is inserted above the content after the page has drawn pushes everything down. There are two clean ways to avoid it.

Take care with the second option for consent banners: many appear only for visitors who have not answered yet, so reserving space for everyone shifts the page for the ones who have. For those, the overlay is the better choice.

Fix 4: make the fallback font match the web font

With font-display: swap, the text first draws in a fallback font and then switches to your web font. If the two fonts have different proportions, every line of text changes size and the page below it moves. Two steps remove most of the shift.

  1. Reduce the number of fonts and weights the top of the page needs, and preload the one or two files it does use. The same settings help First Contentful Paint.
  2. Tune a fallback to match. CSS lets you adjust a local font so it takes the same space as the web font:
@font-face {
  font-family: "Brand Fallback";
  src: local("Arial");
  size-adjust: 104%;       /* scale the glyphs */
  ascent-override: 92%;    /* line metrics of the web font */
  descent-override: 24%;
  line-gap-override: 0%;
}
body { font-family: "Brand", "Brand Fallback", sans-serif; }

The four numbers are examples. Each web font needs its own, which you can calculate from the font's metrics or take from a tool. Chrome's article on font fallbacks lists tools and notes that frameworks such as Next.js do this for you. Support differs by browser: Chrome and Firefox handle all four descriptors, while Safari 17 handles only size-adjust, so a smaller shift remains for Safari visitors. For the full method, with the equations and a worked example, see fixing layout shift from web fonts.

Fix 5: keep a place for late content

Reviews, related products, a "recently viewed" strip, a personalized block: anything that arrives after the first paint needs a placeholder of about the right size. A grey skeleton with the final height is enough. If the height varies, reserve the smallest likely height and let the block grow below it. Avoid inserting late content near the top of the screen, because more elements sit below it and all of them move.

For long pages where you use content-visibility: auto to skip drawing sections that are off screen, also set contain-intrinsic-size to a realistic height. Without it, a section that comes into view changes height and shifts everything after it. It works in Chrome 83 and later, Firefox 107 and later and Safari 17 and later.

Fix 6: animate with transforms

Animating top, left, width, height or margins moves the elements around it and counts as layout shift. Moving something with top or left shifts even when the element is alone in its own layer. Use transform: translate() and scale(), and opacity, which do not change the layout. The same goes for headers that shrink as you scroll: change their appearance with a transform or keep the height fixed.

Step 4: check that it worked

  1. Reload and watch. Throttle the connection in DevTools and look at the first seconds of the load. If something still jumps, you have found the next culprit.
  2. Run the lab test three times. The CLS and the list of culprits should be shorter.
  3. Do the post-load checks. Scroll the whole page, open any menus or tabs, and wait for the late banners. Anything that moves is still a problem.
  4. Check the real-user data in four weeks. The data covers a rolling 28 days.

When a shift is not yours

Some shifts come from outside your page. An ad network that serves different sizes, a consent tool, an embed whose height changes, a browser extension on the tester's machine. Reserve the largest likely space for what you can, tell the vendor about the rest, and accept that a small amount of CLS is normal: the mark is 0.1, not zero.

Check your own page

Run the free audit and look at the layout shift culprits. If an image without dimensions is on the list, fix that first, because it is the cheapest fix. Then load the page on a slow connection and watch for anything that jumps once the page seems to have finished.

Sources

Start here

Free website audit

Live Google data for any public page: mobile and desktop.

Mobile + desktop · Lighthouse lab test · Real-user Chrome data when Google has it

Rather have this done for you? Core Web Vitals · Book a call

Related guides

All guides →