A good Cumulative Layout Shift (CLS) score is 0.1 or less. Anything above 0.25 is "poor" in Google's Core Web Vitals and can hurt both user experience and search rankings. CLS measures unexpected layout movement — the kind that makes users click the wrong button because a banner pushed content down mid-tap.
This guide covers every major CLS cause Lighthouse flags, how to identify the exact shifting elements in your audit, and platform-specific fixes with code you can deploy today.
What Is CLS and Why Does It Matter?
Cumulative Layout Shift is the sum of all unexpected layout movements during a page's lifetime. Each shift is scored as: layout shift score = impact fraction × distance fraction| CLS range | Rating | SEO impact |
|---|---|---|
| 0 – 0.1 | Good | Passes Core Web Vitals |
| 0.1 – 0.25 | Needs improvement | May fail Page Experience in GSC |
| > 0.25 | Poor | Fails Core Web Vitals; ranking penalty possible |
Unlike LCP, which measures loading speed, CLS measures visual stability. A page can load fast and still fail CLS if images, fonts, or widgets push content around after the first paint.
Google uses real-user CLS data from the Chrome User Experience Report (CrUX) for ranking — not just your Lighthouse lab score. But Lighthouse identifies the specific DOM nodes causing shifts, which is what you need to fix them.
How Do I Find What's Causing My CLS?
Run a Lighthouse audit on your URL. In the Diagnostics section, look for:
- Avoid large layout shifts — lists each shifting element with its contribution to total CLS
- Layout shift culprits — in newer Lighthouse versions, shows the node, shift score, and timing
Export a stripped AIReport from PageSpeed Exporter and search the issues array for audits with "id": "layout-shift-elements" or "id": "cls-culprits". Each item includes the selector, shift score, and often a screenshot reference.
Cause 1: Images and Videos Without Dimensions
This is the single most common CLS source on content-heavy sites. When the browser encounters an without width and height, it cannot reserve space until the image downloads. Text and other content render first; then the image loads and pushes everything down.
<img src="/product-hero.webp" alt="Product hero">
After (reserves space immediately):
<img
src="/product-hero.webp"
alt="Product hero"
width="1200"
height="800"
loading="lazy"
>
With responsive CSS, the browser uses the width/height ratio to reserve aspect-ratio space even when the image scales:
img {
max-width: 100%;
height: auto;
}
Next.js Image component handles this automatically when you provide width and height props:
import Image from 'next/image';
<Image
src="/hero.webp"
alt="Hero"
width={1200}
height={630}
priority
/>
WordPress: Enable "Add missing image dimensions" in performance plugins (WP Rocket, Perfmatters), or add dimensions in the theme template. Block editor images often lack dimensions — use a plugin like "Auto Image Attributes" or fix in functions.php.
Shopify: Product images in themes frequently omit dimensions. In Liquid, use image_url with explicit width/height:
<img
src="{{ product.featured_image | image_url: width: 800 }}"
width="800"
height="{{ 800 | divided_by: product.featured_image.aspect_ratio | round }}"
alt="{{ product.title }}"
>
Measured result: On a test WordPress blog with 12 article images missing dimensions, adding width/height dropped CLS from 0.24 → 0.06 — enough to pass Core Web Vitals.
Cause 2: Web Font Reflow (FOUT/FOIT)
When a web font loads after the fallback font renders, text reflows — changing line lengths and pushing content below it. Lighthouse flags this under layout shifts caused by font loading.
Fix withfont-display: swap plus a matched fallback:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
}
Better: use size-adjust to match fallback metrics to your web font, minimizing reflow when the swap happens:
@font-face {
font-family: 'Inter Fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'Inter', 'Inter Fallback', sans-serif;
}
Next.js: With next/font, fonts are self-hosted and preloaded automatically — CLS from fonts is rare when using the built-in loader:
import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });
Preload critical fonts only when necessary — over-preloading hurts LCP:
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
Cause 3: Dynamically Injected Content Above the Fold
Cookie banners, announcement bars, live-chat widgets, and A/B test headers often inject DOM nodes at the top of the page after initial render. Because they appear above existing content, everything below shifts down.
| Widget type | Typical CLS impact | Fix |
|---|---|---|
| Cookie consent bar | 0.05 – 0.15 | Reserve space with fixed min-height container |
| Announcement banner | 0.03 – 0.12 | Render placeholder in SSR; hydrate content into reserved slot |
| Live chat bubble | 0.01 – 0.03 | Fixed position (position: fixed) — does not shift document flow |
| Ad slot (above fold) | 0.10 – 0.30 | Set explicit min-height on ad container |
.announcement-bar-slot {
min-height: 48px; /* match your banner height */
}
.announcement-bar-slot:empty {
display: none; /* no shift when banner is absent */
}
Shopify-specific: Disable or defer apps that inject content above the header — review apps, loyalty bars, and geo-redirect banners are frequent CLS offenders. In the Shopify admin, test by disabling apps one at a time and re-running Lighthouse.
WordPress-specific: Move cookie plugins (CookieYes, Complianz) to load from a reserved footer slot or use their "preload consent bar" settings if available.
Cause 4: Ads, Embeds, and Iframes Without Reserved Space
Ad networks inject iframes asynchronously. Without a reserved container, the ad pushes surrounding content when it finally renders.
<!-- Reserve space before the ad loads -->
<div class="ad-slot" style="min-height: 250px; min-width: 300px;">
<!-- ad script inserts iframe here -->
</div>
For responsive ad units, use CSS aspect-ratio:
.ad-container {
aspect-ratio: 300 / 250;
max-width: 100%;
background: #f3f4f6; /* optional placeholder color */
}
Embedded YouTube videos cause CLS when the iframe lacks dimensions:
<div class="video-wrapper" style="aspect-ratio: 16/9;">
<iframe
src="https://www.youtube.com/embed/VIDEO_ID"
width="560"
height="315"
style="width: 100%; height: 100%;"
loading="lazy"
></iframe>
</div>
Cause 5: CSS Animations That Trigger Layout
Animations on properties like width, height, top, left, margin, and padding cause layout recalculation on every frame. Use transform and opacity instead — they run on the compositor thread without shifting other elements.
.modal {
transition: height 0.3s ease;
}
Does not cause layout shift (preferred):
.modal {
transition: transform 0.3s ease, opacity 0.3s ease;
transform: scale(1);
}
.modal.closed {
transform: scale(0.95);
opacity: 0;
}
Lighthouse's "Avoid non-composited animations" audit correlates with CLS issues on animated elements.
Cause 6: Late-Loading CSS That Changes Layout
Stylesheets loaded after first paint can change element sizes — especially utility frameworks or theme CSS that arrives late. This is less common with modern bundlers but still appears on WordPress sites with multiple plugin stylesheets.
Fixes:- Inline critical above-the-fold CSS
- Defer non-critical stylesheets (see render-blocking guide)
- Avoid
@importin CSS — each@importis a render-blocking chain
Platform Quick-Reference: Top CLS Fixes
| Platform | #1 CLS cause | Fastest fix |
|---|---|---|
| WordPress | Images without dimensions + cookie plugins | Add dimensions; reserve banner space |
| Shopify | App widgets + product images | Audit apps; fix Liquid image tags |
| Next.js | Third-party scripts + missing Image props | Use next/image; defer widgets |
| Webflow | Embed blocks + custom code injections | Set fixed heights on embed wrappers |
| Plain HTML | Missing width/height on all media | Add dimensions to every and |
How to Verify Your CLS Fix
- Re-run Lighthouse on the same URL and strategy (mobile vs desktop)
- Confirm CLS in metrics is ≤ 0.1
- Check "Avoid large layout shifts" — remaining items should be negligible (< 0.01 each)
- Compare lab CLS to CrUX field data in your export — field data reflects real users over 28 days, so improvements take time to appear in Google Search Console
- Use Chrome DevTools → Performance → check "Experience" row for Layout Shift markers during page load
For before/after tracking on Starter plans, re-scan the same URL and use the comparison view to confirm CLS dropped.
How Do I Fix CLS Using AI?
If you have multiple shifting elements across a complex site, export your Lighthouse data and ask an AI agent to prioritize by shift score:
I'm attaching my Lighthouse AIReport for example.com.
My CLS score is 0.31 (poor). I need to get it under 0.1.
1. List every layout shift culprit sorted by impact score
2. For each element, explain why it shifts and provide the exact HTML/CSS fix
3. Tell me which 3 fixes will reduce CLS the fastest
I'm using [WordPress / Shopify / Next.js].
See How to Fix Core Web Vitals Using ChatGPT for the full AI workflow, or Use Cursor with Lighthouse for an in-editor approach.
Further Reading
- What is CLS? — thresholds, causes, and glossary definition
- What Are Core Web Vitals? — how CLS fits with LCP and INP
- How to Diagnose Slow LCP — loading issues often pair with CLS on the same page
- Shopify Speed Optimization 2026 — platform-specific CLS fixes for stores
- WordPress Core Web Vitals 2026 — plugin and theme CLS patterns
- 5 Quick Wins to Improve Lighthouse Score — includes dimension and font fixes