TL;DR
Layout shift happens when content appears or changes size after the page has started rendering and pushes visible content around. The usual causes are images and iframes without dimensions, ads and embeds, web fonts swapping in, banners injected at the top of the page, and animations of layout properties. The fix is almost always to reserve space before the content arrives, or to move things with transforms instead of layout.
Key takeaways
- Give every image, video and iframe width and height attributes or a CSS aspect-ratio so the browser can reserve space.
- Reserve a fixed slot for ads and embeds, and keep it even when no ad fills it.
- Match fallback font metrics with size-adjust and the override descriptors so the web font swap does not reflow text.
- Show cookie notices and promo bars as fixed overlays, or render them in the initial HTML, instead of inserting them above content.
- Animate transform and opacity, not top, left, width, height or margin.
- Lighthouse only sees shifts during load, so check field CLS for shifts that happen while people scroll and interact.
Most layout shift comes from content that appears or changes size after the page has started rendering: images and iframes without dimensions, ads and embeds, web fonts swapping in, banners inserted at the top of the page, and animations of layout properties. The fix is nearly always the same idea. Reserve the space before the content arrives, or move things in a way that does not affect the layout around them.
What counts as a layout shift
A layout shift is any unexpected movement of a visible element from one rendered frame to the next. Cumulative Layout Shift scores each shift by how much of the viewport moved and how far it moved, then groups shifts that happen close together into bursts. CLS is the score of the worst burst during the page’s life.
A few details matter when you debug it:
- Shifts that happen within 500 ms of a click, tap or key press are excluded, because the user expects the page to change.
- Changes made with CSS
transformdo not count, because they do not move other elements. - Content added below the visible area does not count until it moves something on screen.
As a Core Web Vital, CLS is good at 0.1 or less and poor above 0.25, measured on real users at the 75th percentile.
Lab CLS and field CLS can disagree
Lighthouse measures CLS during the initial page load only. It does not scroll, click or wait for the ad that arrives eight seconds later. Real visitors do all of those things, so field CLS often comes out worse than the lab number.
If Lighthouse says 0.02 and Search Console says 0.3, the shift is probably happening after load: lazy-loaded images further down the page, ads refreshing, or content appearing as people scroll. Lab data vs field data explains why the two sources differ and when to trust each.
How to find what is shifting
Before fixing anything, find the element that moves and the element that caused the move.
- Lighthouse. The layout shift diagnostic lists the elements that shifted most and, where it can tell, a likely cause, such as an unsized image or a web font loading.
- Chrome DevTools. In the Rendering panel, turn on Layout Shift Regions to highlight areas as they move. The Performance panel shows each shift on a timeline next to the network request that triggered it.
- Field data. The
web-vitalsJavaScript library has an attribution build that reports which element caused the largest shift for real visitors.
Once you know the cause, it almost always falls into one of the groups below.
1. Images without dimensions
An <img> without width and height takes up no height until the file downloads. When it arrives, the browser makes room for it and pushes everything below it down.
Fix: add width and height attributes that match the image’s aspect ratio. Modern browsers use them to reserve the right amount of space, even when CSS makes the image responsive.
img { max-width: 100%; height: auto; }
<img src="/img/photo.webp" width="1200" height="800" alt="...">
For images whose size you do not know in advance, set a CSS aspect-ratio on the container. In a <picture> element, each <source> can carry its own width and height when the cropped versions have different shapes. Fix this in the image component of your template, not one page at a time; template-level fixes explains why that one change can cover hundreds of pages.
2. Iframes, videos and embeds
Iframes, videos, maps and social embeds behave like unsized images, and some resize themselves again after their own scripts run.
Fix:
- Give iframes and
<video>elements explicit dimensions, or wrap them in a container withaspect-ratio: 16 / 9. - For embeds that resize after load, reserve a
min-heightclose to their usual final size. - Consider a facade: show a static preview image and load the real embed only when someone clicks it.
3. Ads
Ads are one of the hardest sources of CLS because their size is often unknown until the ad server responds, and some slots collapse when no ad fills them.
Fix:
- Reserve the slot size with
min-height(andmin-widthif needed) before the ad loads. - For slots that accept several sizes, reserve space for the most likely one.
- Keep the reserved space when no ad is returned. Show a placeholder instead of collapsing the slot.
- Avoid placing ads at the very top of the content, where any change in size moves everything below.
4. Web fonts that load late
Fonts cause shifts when the fallback font and the web font take up different amounts of space. With FOUT (flash of unstyled text), text first renders in the fallback and then swaps to the web font, reflowing lines and pushing content. With FOIT (flash of invisible text), text is hidden while the font loads, and it can still change the layout when it appears.
Fix:
- Preload the one or two font files used above the fold so they arrive early.
- Use
font-display: optionalfor body text if you can accept the fallback on slow connections. The browser only uses the web font if it arrives almost immediately, so there is no late swap. - Match the fallback’s metrics. Define a fallback
@font-facewithsize-adjust,ascent-override,descent-overrideandline-gap-overrideso it takes up the same space as your web font.
@font-face {
font-family: "Brand Fallback";
src: local("Arial");
size-adjust: 104%;
ascent-override: 92%;
descent-override: 24%;
line-gap-override: 0%;
}
body { font-family: "Brand", "Brand Fallback", sans-serif; }
The values above are examples. Calculate them for your own font, and treat them as an improvement rather than a guarantee, since browser support for the override descriptors varies.
5. Injected banners and cookie notices
A cookie notice, promo bar or “download our app” banner inserted at the top of the page after load pushes every visible element down. It is one of the most common shifts on sites that otherwise look clean.
Fix:
- Show notices as a
position: fixedorstickyoverlay, usually at the bottom of the screen. An overlay does not move other content. - If a banner must sit in the page flow, render it in the initial HTML so the space is there from the first paint.
- Do not insert a bar at the top after a geolocation or personalization request returns. Reserve the space, or show the message somewhere it cannot push content.
6. Animating layout properties
Animating top, left, width, height, margin or padding changes the layout on every frame, and each frame can count as a shift for the elements around it.
Fix: animate transform (translate, scale) and opacity instead. A slide-in panel moved with transform: translateY() looks the same to the user, does not shift its neighbors, and is cheaper for the browser to render.
7. Dynamic content above existing content
Anything that loads late and lands above content people are already looking at will shift it: a “related products” row, a recommendation widget, a review summary, a newsletter box, or a component that renders only after hydration.
Fix:
- Reserve space with a placeholder or skeleton of the same final size.
- Insert late content below the viewport, or below the content it relates to, rather than above it.
- Make user-triggered changes, such as “load more”, respond within 500 ms so they fall inside the input exclusion window.
Quick reference
| Cause | Why it shifts | Fix |
|---|---|---|
| Image without dimensions | No space reserved until it downloads | width and height, or aspect-ratio |
| Iframe or embed | Unknown size, resizes after load | Explicit size, min-height, facade |
| Ad slot | Size decided by the ad server | Reserved slot that never collapses |
| Web font | Fallback and web font differ in size | Preload, font-display: optional, size-adjust |
| Banner or cookie notice | Inserted into the flow above content | Fixed overlay or server-rendered space |
| Layout animation | Changes layout every frame | Animate transform and opacity |
| Late dynamic content | Lands above content already on screen | Skeleton of the final size, insert below |
How to check layout shift across your whole site
Manually, run Lighthouse on a few pages from each template, turn on Layout Shift Regions in DevTools while you scroll, and compare with the Core Web Vitals report in Search Console, which groups similar URLs and uses real-user data. Because templates repeat, a shift on one product page usually means a shift on all of them, which is why a single-URL test is not enough, as explained in Lighthouse only tests one page.
If you want every page checked without scripting, AiroPages runs Lighthouse on every page on mobile and desktop, groups identical layout shift issues by template, and shows field data next to lab results where Google has it, so you can see which shifts only appear for real visitors.
Fix the space, not the symptom
Layout shift is rarely mysterious once you find the element that moved. Reserve space for anything that loads late, keep banners out of the flow, animate with transforms, and fix the shared component instead of each page. Then re-test after changes, and keep an eye on field CLS, since it is the number that counts for Core Web Vitals.
Frequently asked questions
What causes layout shift on a website?
Layout shift is caused by content that appears or resizes after the page starts rendering and moves other visible content. Common causes are images and iframes without dimensions, ads and embeds that load late, web fonts that swap in with different metrics, banners inserted at the top of the page, and animations of properties like height or top.
What is a good CLS score?
A CLS of 0.1 or less is good, between 0.1 and 0.25 needs improvement, and above 0.25 is poor. For Core Web Vitals, Google measures it on real users at the 75th percentile over a rolling 28 days.
Why does Lighthouse show a good CLS while Search Console says it is poor?
Lighthouse only measures layout shifts during the initial page load, with no scrolling or clicking. Real visitors scroll, which triggers lazy-loaded images, ads and embeds that can shift content later. Search Console uses field data from real Chrome users, so it catches shifts that a lab test never sees.
Do cookie banners cause layout shift?
They do if they are inserted into the normal page flow above existing content, because everything below them moves down. A banner shown as a fixed or sticky overlay, usually at the bottom of the screen, does not push other content and does not count as a layout shift.
Does font-display swap cause CLS?
It can. With swap, text first renders in a fallback font and then re-renders in the web font, and if the two fonts have different sizes the text reflows and shifts nearby content. Preloading the font, using font-display optional, or matching the fallback's metrics with size-adjust reduces or removes the shift.
Published