Cumulative Layout Shift (CLS) measures how much page content moves unexpectedly while loading. A CLS score above 0.1 means users experience jarring content jumps — text shifting down as images load, buttons moving as ads inject, or the entire page reflowing when fonts swap. This guide covers how to identify and fix every common CLS source.
Why CLS matters for SEO
CLS is one of three Core Web Vitals, which are confirmed Google ranking signals. Pages with CLS above 0.25 are rated "poor" and may lose ranking position to competitors with stable layouts.
Beyond rankings, layout shifts erode trust. Users who click a button only to have a different element appear under their finger lose confidence in the site. High CLS correlates with higher bounce rates and lower conversion rates.
| CLS Score | Rating | User Experience |
|---|---|---|
| 0 – 0.1 | Good | Layout feels stable |
| 0.1 – 0.25 | Needs improvement | Occasional noticeable shifts |
| > 0.25 | Poor | Frequent disruptive layout jumps |
Step 1: Measure your current CLS
Field data (real users)
Check your CLS in Google Search Console under the Core Web Vitals report. This shows real-user data grouped by URL pattern.
For per-page data, use the Chrome UX Report (CrUX) via PageSpeed Insights:
https://pagespeed.web.dev/analysis?url=https://yoursite.com/page
Lab data (controlled testing)
Run Lighthouse in Chrome DevTools:
- Open DevTools → Lighthouse tab
- Select "Performance" category
- Run audit — CLS appears in the Diagnostics section
For visual debugging, use the Performance tab:
- Record a page load
- Enable "Layout Shifts" in the Experience section
- Each shift shows as a blue marker on the timeline
The Layout Instability API
Monitor CLS in production with JavaScript:
let clsValue = 0
new PerformanceObserver(list => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
clsValue += entry.value
}
}
}).observe({ type: "layout-shift", buffered: true })
The hadRecentInput filter excludes shifts caused by user interaction (clicking, typing), which don't count toward the CLS score.
Step 2: Fix images and media
Images without explicit dimensions are the #1 cause of layout shifts. When the browser first renders the page, it doesn't know how tall the image will be, so it allocates zero space. When the image loads, everything below it shifts down.
Always set width and height
<!-- Bad: browser doesn't know dimensions until image loads -->
<img src="hero.jpg" alt="Hero image" />
<!-- Good: browser reserves space immediately -->
<img src="hero.jpg" alt="Hero image" width="1200" height="630" />
Modern browsers use the width/height attributes to calculate the aspect ratio and reserve the correct space, even with responsive CSS.
Use aspect-ratio CSS
For responsive images where you don't want fixed pixel dimensions:
.hero-image {
aspect-ratio: 16 / 9;
width: 100%;
height: auto;
}
Video embeds
YouTube and Vimeo embeds cause massive shifts. Wrap them in an aspect-ratio container:
.video-wrapper {
aspect-ratio: 16 / 9;
width: 100%;
background: #1a1a1a;
}
.video-wrapper iframe {
width: 100%;
height: 100%;
}
Step 3: Fix web font loading
When a web font loads and replaces the fallback system font, text reflows. Different fonts have different metrics (character widths, line heights), causing paragraphs to expand or shrink.
Use font-display: swap with size-adjust
@font-face {
font-family: "Custom Font";
src: url("/fonts/custom.woff2") format("woff2");
font-display: swap;
/* Reduce reflow by matching fallback metrics */
size-adjust: 105%;
ascent-override: 90%;
descent-override: 20%;
line-gap-override: 0%;
}
Preload critical fonts
<link
rel="preload"
href="/fonts/custom.woff2"
as="font"
type="font/woff2"
crossorigin
/>
Preloading reduces the window between fallback and custom font rendering, minimizing the visible shift.
Use font metric overrides
Tools like Fontaine automatically calculate size-adjust, ascent-override, and descent-override values to match your web font's metrics to the fallback font. This nearly eliminates font-swap layout shifts.
Step 4: Fix dynamic content injection
Content that inserts into the DOM after initial render pushes existing content down.
Reserve space for ads
.ad-slot {
min-height: 250px; /* Match the ad unit height */
width: 100%;
background: #f5f5f5;
}
If the ad doesn't fill, the reserved space remains — better than a shift.
Reserve space for cookie banners
Position cookie consent banners in a way that doesn't push content:
/* Fixed position: overlays content instead of pushing it */
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 1000;
}
Avoid inserting content above existing content
If you must inject elements (notifications, banners), append them to the top of the viewport with position: fixed or insert them below the current scroll position where the shift won't be visible.
Step 5: Fix CSS containment issues
Use CSS containment
The contain property tells the browser that an element's layout is independent of the rest of the page:
.card {
contain: layout style;
}
This prevents changes inside .card from causing layout recalculations in other parts of the page.
Use content-visibility for below-fold content
.below-fold-section {
content-visibility: auto;
contain-intrinsic-size: 0 500px; /* Estimated height */
}
content-visibility: auto skips rendering for off-screen elements. The contain-intrinsic-size provides a height estimate so the scrollbar doesn't jump.
Step 6: Fix late-loading CSS
CSS that loads after the initial render can change element sizes and positions.
Inline critical CSS
Extract the CSS needed for above-the-fold content and inline it in the <head>:
<head>
<style>
/* Critical CSS for above-fold content */
.header {
height: 64px;
}
.hero {
min-height: 400px;
}
</style>
<link
rel="stylesheet"
href="/styles.css"
media="print"
onload="this.media='all'"
/>
</head>
Avoid CSS-in-JS runtime injection
CSS-in-JS libraries that inject styles at runtime (styled-components without SSR, Emotion without extraction) cause layout shifts because elements render unstyled first, then reflow when styles inject.
Solutions:
- Use SSR/SSG to extract styles at build time
- Use static CSS extraction (vanilla-extract, Tailwind CSS)
- Use CSS Modules with bundler extraction
Step 7: Prevent animation-induced shifts
Use transform instead of position/size properties
/* Bad: changes layout (causes shifts) */
.expand {
height: 0;
transition: height 0.3s;
}
.expand.open {
height: 200px;
}
/* Good: doesn't affect layout */
.expand {
transform: scaleY(0);
transform-origin: top;
transition: transform 0.3s;
}
.expand.open {
transform: scaleY(1);
}
Properties that trigger layout shifts: width, height, top, left, margin, padding, border-width.
Properties safe from layout shifts: transform, opacity, filter, clip-path.
CLS debugging checklist
- Run PageSpeed Insights and note the CLS score and flagged elements
- Open DevTools → Performance → record page load → check Layout Shifts in Experience lane
- For each shift, identify the element that moved and what caused it:
- Image without dimensions? → Add width/height
- Font swap? → Add font metric overrides
- Ad injection? → Reserve space with min-height
- Dynamic content? → Use fixed positioning or reserve space
- Late CSS? → Inline critical styles
- Fix, redeploy, and re-measure
- Monitor CrUX data for 28 days to confirm field improvement
Frequently Asked Questions
What's a realistic CLS target?
Aim for under 0.05. The "good" threshold is 0.1, but the best-performing sites achieve 0.01-0.03. Zero CLS is difficult to achieve on pages with ads or dynamic content.
Do user-triggered layout shifts count toward CLS?
No. Shifts within 500ms of a user interaction (click, tap, keypress) are excluded from the CLS calculation. Only unexpected shifts count.
Does CLS affect mobile and desktop differently?
Yes. Mobile devices have smaller viewports, so the same pixel shift creates a larger proportional impact. Mobile CLS scores are typically higher than desktop for the same page.
How quickly do CLS improvements affect rankings?
Google updates CWV data on a 28-day rolling window. After fixing CLS issues, allow 4-6 weeks for the improvement to reflect in Search Console and potentially influence rankings.
Can lazy loading cause CLS?
Yes, if lazy-loaded images don't have explicit dimensions. The image container has zero height until the image loads, then expands and shifts content below. Always set width/height on lazy-loaded images.
Related Resources
- Understanding CLS — What CLS is and how it's calculated
- Preventing Layout Shifts — Architectural patterns for shift-free pages
- Cumulative Layout Shift — CLS glossary definition
- Performance Audit Template — Run a structured performance audit