You go to tap a button, an ad loads above it, and you hit the wrong link. That jump is a layout shift, and Cumulative Layout Shift (CLS) is how Google scores it. The good news: CLS has a short list of causes, and most fixes are a few lines of HTML or CSS. This guide shows how to find which element is shifting and how to fix each common culprit.
The target is a CLS of 0.1 or less at the 75th percentile of real visits. Above 0.25 is rated poor.
Key takeaways
- Aim for a CLS of 0.1 or less at the 75th percentile of real visits, as anything above 0.25 is rated poor.
- Always set width and height attributes on images and videos so the browser can reserve the right space before they load.
- Reserve space for ads, embeds and iframes, and show banners as fixed overlays rather than inserting them above content.
- Reduce font-related shifts by preloading the main font file, using font-display optional or matching the fallback font metrics.
How CLS is scored, briefly
Each unexpected shift gets a score of impact fraction x distance fraction: roughly, how much of the screen moved and how far. Shifts are grouped into windows (bursts less than 1 second apart, up to 5 seconds long), and your CLS is the worst window.
Two rules matter when debugging:
- Shifts within 500 ms of a tap, click or key press do not count. Expanding an accordion when the user clicks it is fine.
- Scrolling is not user input for this purpose. Content that jumps while someone scrolls still counts, which is why field CLS is often worse than the lab result.
Step 1: Find what is shifting
In the lab
- Lighthouse / PageSpeed Insights: look at the "Avoid large layout shifts" diagnostic, which lists the elements that moved, and "Image elements do not have explicit width and height".
- Chrome DevTools > Performance panel: record a page load. The Layout Shifts track shows each shift cluster. Click one to see the score and the affected elements highlighted on the page. The Insights sidebar includes "Layout shift culprits", which points at likely causes such as unsized images or web fonts.
- Live metrics view: the Performance panel shows CLS live while you scroll and click, which catches shifts that happen after load.
In the field
If PageSpeed Insights shows good lab CLS but poor field CLS, the shift is happening after the initial load: on scroll, after a delayed banner, or when an ad refreshes. To catch those, log shifts from real users with the web-vitals library's attribution build, or paste this into the console and use the page normally:
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.hadRecentInput) continue;
console.log('Shift', entry.value.toFixed(4),
entry.sources?.map((s) => s.node));
}
}).observe({ type: 'layout-shift', buffered: true });
Fix 1: Give images and videos dimensions
When the browser does not know an image's size, it reserves zero height, then pushes everything down once the image arrives. The fix is to always set width and height attributes. Modern browsers use them to calculate the aspect ratio before the image loads, even if CSS makes the image responsive.
<img src="/img/team.webp" width="1200" height="800" alt="Our team">
img, video {
max-width: 100%;
height: auto;
}
The attributes do not need to match the displayed size, only the correct ratio. If you're unsure of the ratio, our aspect ratio calculator works it out from any width and height and gives you the CSS rule. For CSS backgrounds or containers whose content arrives later, use aspect-ratio:
.hero-media {
aspect-ratio: 16 / 9;
width: 100%;
}
WordPress adds width and height to images inserted through the editor, but images added by page builders, sliders or custom theme code often lack them. Check the Lighthouse diagnostic rather than assuming.
Fix 2: Reserve space for ads, embeds and iframes
Ads, YouTube embeds, maps, social posts and review widgets usually arrive after the surrounding content. Without reserved space they push the page down.
/* Reserve the smallest likely ad size */
.ad-slot {
min-height: 250px;
}
/* Responsive video or map embed */
.embed {
aspect-ratio: 16 / 9;
width: 100%;
}
.embed iframe {
width: 100%;
height: 100%;
}
Practical rules:
- Keep the space even when no ad fills. Collapsing an empty slot causes its own shift. Show a placeholder instead.
- Size for the most common ad, not the smallest, if your ad slot serves multiple sizes. Look at your ad reports for which size fills most often.
- Avoid ad slots near the top of the viewport. A shift at the top moves everything below it, so it scores worse.
- Third-party widgets such as chat bubbles or review carousels should use
position: fixedor a reserved container, never be inserted into the flow above existing content.
Fix 3: Stop injecting content above existing content
Cookie consent banners, "free shipping" bars, newsletter prompts and "app available" banners are frequent CLS offenders when they are inserted at the top of the page after it renders.
Better options:
- Overlay instead of insert. A banner with
position: fixed(bottom of the screen) does not move other content. - Render it server-side in the initial HTML so space exists from the first paint.
- Reserve the space with a fixed-height container if it must sit in the flow.
.cookie-banner {
position: fixed;
inset: auto 0 0 0;
z-index: 1000;
}
For infinite scroll or "related posts" that load in, append them below the viewport or use a "Load more" button. Content that loads in response to a click is exempt for 500 ms.
Fix 4: Tame web font swaps
When a custom font replaces a fallback font with different metrics, text reflows: lines get longer or shorter, and everything below moves. There are three tools for this.
Preload the main font file
<link rel="preload" href="/fonts/brand.woff2" as="font" type="font/woff2" crossorigin>
The crossorigin attribute is required for font preloads, even for same-origin fonts. Preload only the one or two files used above the fold.
Choose a font-display strategy
font-display: optionaluses the web font only if it is ready almost immediately; otherwise the fallback stays for that page view. This all but removes font-related shifts.font-display: swapalways swaps in the web font, which can cause a shift unless the fallback metrics are matched.
Match the fallback font's metrics
Use size-adjust and the override descriptors so the fallback takes up the same space as the web font:
@font-face {
font-family: "Brand";
src: url("/fonts/brand.woff2") format("woff2");
font-display: swap;
}
@font-face {
font-family: "Brand Fallback";
src: local("Arial");
size-adjust: 105%; /* example values - tune for your font */
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: "Brand", "Brand Fallback", sans-serif;
}
The percentages differ for every font pairing. Frameworks such as Next.js (next/font) calculate them automatically, and there are free online fallback-font generators that do the same.
Fix 5: Animate with transform, not layout properties
Animating top, left, margin, height or width moves surrounding content and can be counted as layout shifts. transform and opacity do not affect layout.
/* Causes layout work */
.panel { transition: top 0.3s; }
/* Does not */
.panel { transition: transform 0.3s; }
.panel.open { transform: translateY(0); }
Fix 6: Make pages eligible for the back/forward cache
When a page is restored from the back/forward cache (bfcache), its CLS starts again from zero and the load-time shifts do not repeat. Common blockers include unload event handlers (use pagehide instead) and Cache-Control: no-store on the HTML document. Chrome DevTools > Application > Back/forward cache has a "Run test" button that checks the current page and lists blockers.
Common CLS causes at a glance
| Culprit | Fix |
|---|---|
| Images without size | width and height attributes, height: auto in CSS |
| Ads and embeds | min-height or aspect-ratio on a container |
| Injected banners | position: fixed overlay or server-render with reserved space |
| Font swap | Preload, font-display: optional, metric-matched fallback |
| Animated layout properties | Use transform |
| Shifts only in field data | Debug post-load behavior: scroll, ad refresh, delayed popups |
Frequently asked questions
Why is my lab CLS 0 but field CLS poor?
Lighthouse only measures the initial load of a single simulated visit. Real users scroll, wait for delayed banners, and see ads refresh, all of which can shift content later. Use the DevTools live metrics view while scrolling, or collect attribution data from real users.
Do layout shifts after a click count?
Not if they happen within 500 ms of the click, tap or key press, because the user expects the page to change. Shifts that happen later, for example after a slow network request completes, do count.
Does lazy loading cause CLS?
Lazy loading itself does not, as long as the images have width and height attributes so space is reserved. Lazy-loaded images without dimensions are a common cause of shifts as the user scrolls.
Is a CLS of 0.1 good enough?
Yes. 0.1 or less at the 75th percentile is rated good. Aiming for close to zero gives you margin for ad variation and slower devices.
What to do next
Record a page load in the DevTools Performance panel, click the biggest layout shift cluster, and fix that element first. Then scroll the page with live metrics on to catch shifts the lab missed. Related reading: Core Web Vitals explained and image optimization with WebP and AVIF. To see your CLS alongside the rest of your performance, SEO and security checks, run a free audit.
Related reading
- Why Is My Website So Slow? Common Causes and How to Fix Them: Why is my website slow? Usually slow hosting, heavy images, too much JavaScript or third-party scripts. Find the real cause in minutes and fix it in order.
- WordPress Core Web Vitals: Which Plugins Actually Help: Improve WordPress Core Web Vitals with the right plugin types for LCP, INP and CLS, a before-and-after testing method, and the plugin overlaps to avoid.
Sources
Written by the MakeItFlow team
We build the website audit these guides are based on. Every fact is checked against the primary sources listed above and this guide was last reviewed on 30 October 2026 . How we research and check
Check your own site in 30 seconds
Run a free audit covering speed, SEO, DNS, email authentication and security headers — and get a prioritised fix list.