Cumulative Layout Shift (CLS) is one of the three Core Web Vitals metrics that measures the visual stability of a page. Unexpected element shifts frustrate users and can negatively impact Google rankings. This guide will help you identify and fix CLS issues.
What is CLS?
CLS measures the sum of all unexpected layout shifts that occur during the entire page lifecycle. A shift is “unexpected” when an element changes position without user interaction (e.g., clicking).
How is CLS Calculated?
CLS = sum of (impact fraction × distance fraction) for each shift
- Impact fraction - percentage of viewport occupied by the shifted element
- Distance fraction - shift distance as a percentage of the viewport
CLS Thresholds
| Score | Rating |
|---|---|
| ≤ 0.1 | Good (green) |
| 0.1 - 0.25 | Needs Improvement (orange) |
| > 0.25 | Poor (red) |
Most Common Causes of CLS
1. Images Without Dimensions
When the browser doesn’t know an image’s dimensions, it reserves 0px height, and a shift occurs after the image loads.
<!-- Bad - no dimensions -->
<img src="photo.jpg" alt="Photo">
<!-- Good - dimensions specified -->
<img src="photo.jpg" alt="Photo" width="800" height="600">
<!-- Good - aspect-ratio in CSS -->
<img src="photo.jpg" alt="Photo" style="aspect-ratio: 4/3; width: 100%;">
2. Ads and Embeds
Ads often load with a delay and have dynamic height.
<!-- Reserve space for ads -->
<div class="ad-container" style="min-height: 250px;">
<!-- Ad code -->
</div>
3. Dynamically Injected Content
Cookie banners, push notifications, toolbars - anything that appears after the page loads.
/* Instead of pushing content, use overlay */
.cookie-banner {
position: fixed;
bottom: 0;
/* DON'T use position: relative at the top of the page */
}
4. Web Fonts (FOUT/FOIT)
Swapping the fallback font for the target font can change text size.
/* Match fallback to target font */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: swap;
/* Use size-adjust for matching */
size-adjust: 100%;
ascent-override: 90%;
descent-override: 20%;
line-gap-override: 0%;
}
/* Or use font-display: optional */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: optional; /* Don't show fallback if font doesn't load quickly */
}
5. Animations Causing Reflow
Animations on width, height, top, left cause reflow and can affect CLS.
/* Bad - height animation */
.menu {
transition: height 0.3s;
}
/* Good - transform animation */
.menu {
transition: transform 0.3s;
transform-origin: top;
}
.menu.collapsed {
transform: scaleY(0);
}
CLS Debugging Tools
1. Chrome DevTools - Performance
- Open DevTools (F12)
- Go to the Performance tab
- Check Web Vitals
- Press Ctrl+Shift+E (record with reload)
- Look for red Layout Shift markers
2. Chrome DevTools - Rendering
- Open DevTools
- Press Ctrl+Shift+P
- Type “Show Rendering”
- Check Layout Shift Regions
- Blue rectangles show shifts in real-time
3. Web Vitals Extension
Chrome extension showing CLS, LCP, and INP in real-time: Web Vitals Extension
4. Lighthouse
# CLI
npx lighthouse https://example.com --only-categories=performance
# Or in Chrome DevTools → Lighthouse
5. PageSpeed Insights
https://pagespeed.web.dev/ - shows CLS from both lab and field data (CrUX).
6. Layout Instability API
Programmatic detection of shifts:
// Listen for layout shifts
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
console.log('Layout shift detected:', entry);
console.log('Value:', entry.value);
console.log('Sources:', entry.sources);
// Show which elements shifted
entry.sources?.forEach(source => {
console.log('Element:', source.node);
console.log('Previous rect:', source.previousRect);
console.log('Current rect:', source.currentRect);
});
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });
7. web-vitals Library
import { onCLS } from 'web-vitals';
onCLS(console.log, { reportAllChanges: true });
// { name: 'CLS', value: 0.15, rating: 'needs-improvement', entries: [...] }
CLS Fix Techniques
1. Reserving Space for Images
Method 1: width and height attributes
<img src="hero.jpg" width="1200" height="600" alt="Hero">
The browser will calculate the aspect ratio and reserve space.
Method 2: CSS aspect-ratio
.hero-image {
aspect-ratio: 16 / 9;
width: 100%;
height: auto;
}
Method 3: Padding hack (legacy)
.image-container {
position: relative;
padding-bottom: 56.25%; /* 16:9 = 9/16 = 0.5625 */
}
.image-container img {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
object-fit: cover;
}
2. Reserving Space for Ads
.ad-slot {
min-height: 250px; /* Standard ad height */
background: #f0f0f0; /* Placeholder */
}
/* For responsive ads */
.ad-slot-responsive {
min-height: 100px;
}
@media (min-width: 768px) {
.ad-slot-responsive {
min-height: 250px;
}
}
3. Skeleton Screens
Instead of empty space, show a skeleton:
<div class="card skeleton">
<div class="skeleton-image"></div>
<div class="skeleton-text"></div>
<div class="skeleton-text short"></div>
</div>
<style>
.skeleton-image {
aspect-ratio: 16/9;
background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
background-size: 200% 100%;
animation: shimmer 1.5s infinite;
}
@keyframes shimmer {
0% { background-position: 200% 0; }
100% { background-position: -200% 0; }
}
</style>
4. Fonts - Minimizing FOUT
Preload fonts:
<link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2" crossorigin>
font-display: optional:
@font-face {
font-family: 'Inter';
src: url('/fonts/inter.woff2') format('woff2');
font-display: optional;
}
Font matching with CSS Font Loading API:
document.fonts.ready.then(() => {
document.body.classList.add('fonts-loaded');
});
/* Before fonts load - fallback with matched metrics */
body {
font-family: 'Inter Fallback', sans-serif;
}
/* After loading */
body.fonts-loaded {
font-family: 'Inter', sans-serif;
}
5. Dynamic Content - Transform Instead of Reflow
/* Bad - adding element pushes content */
.notification {
position: relative;
}
/* Good - overlay doesn't affect layout */
.notification {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 1000;
}
/* Or animation with transform */
.notification {
position: fixed;
top: 0;
transform: translateY(-100%);
transition: transform 0.3s;
}
.notification.visible {
transform: translateY(0);
}
6. Lazy Loading with Placeholder
<div class="lazy-container" style="aspect-ratio: 16/9;">
<img
src="placeholder.jpg"
data-src="actual-image.jpg"
loading="lazy"
alt="Description"
>
</div>
// Intersection Observer for lazy loading
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target;
img.src = img.dataset.src;
observer.unobserve(img);
}
});
});
document.querySelectorAll('img[data-src]').forEach(img => {
observer.observe(img);
});
7. Avoiding document.write()
// Bad - document.write blocks parser
document.write('<script src="ad.js"></script>');
// Good - dynamic insertion
const script = document.createElement('script');
script.src = 'ad.js';
script.async = true;
document.body.appendChild(script);
Debugging Specific Scenarios
Scenario 1: CLS from Google Fonts
Problem: Text changes size after font loads.
Diagnosis:
- Open DevTools → Network
- Filter by “font”
- Check font timing vs FCP
Solution:
<!-- Preconnect to Google Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<!-- Or self-hosting -->
<link rel="preload" href="/fonts/roboto.woff2" as="font" type="font/woff2" crossorigin>
Scenario 2: CLS from Lazy-loaded Images
Problem: Images below the fold cause CLS while scrolling.
Diagnosis:
- Enable Layout Shift Regions in Rendering
- Scroll the page
- Observe blue rectangles
Solution:
<!-- Always specify dimensions -->
<img
src="photo.jpg"
loading="lazy"
width="400"
height="300"
alt="Photo"
>
Scenario 3: CLS from Dynamic Banners
Problem: Cookie banner/notification pushes content.
Diagnosis:
- Refresh the page
- Observe the moment the banner appears
- Check if content shifts
Solution:
/* Banner at bottom as overlay */
.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
z-index: 9999;
}
/* Or reserve space at top */
.page-wrapper {
padding-top: 60px; /* Banner height */
}
Scenario 4: CLS from Iframes (Embeds)
Problem: YouTube embeds, maps, social widgets.
Solution:
<div class="video-container" style="aspect-ratio: 16/9;">
<iframe
src="https://www.youtube.com/embed/VIDEO_ID"
loading="lazy"
style="width: 100%; height: 100%;"
frameborder="0"
allowfullscreen
></iframe>
</div>
CLS Fix Checklist
Images and Media
- All
<img>tags have width and height - Using aspect-ratio for responsive images
- Iframes have defined dimensions
- Video posters are loaded
Fonts
- Fonts are preloaded
- Using font-display: swap or optional
- Fallback font has similar metrics
Ads and Embeds
- Ad slots have min-height
- Social embeds have placeholders
- Lazy-loaded content has reserved space
Dynamic Content
- Banners use position: fixed
- Notifications don’t push content
- Animations use transform instead of reflow
JavaScript
- No document.write()
- Dynamically added content has placeholder
- Skeleton screens for loading components
Summary
CLS is a metric that directly impacts user experience. Unexpected shifts are frustrating and can lead to accidental clicks. Key principles:
- Always specify dimensions for images and media
- Reserve space for dynamic content
- Optimize fonts - preload and font-display
- Use position: fixed for overlays
- Monitor CLS in real data (CrUX)
The goal is CLS below 0.1 for 75% of users. Regular testing and debugging will help maintain a stable layout. To see how CLS fits into the broader landscape of web performance and SEO, explore our complete guide to web technologies and Google rankings.
Sources
-
web.dev - Cumulative Layout Shift (CLS) https://web.dev/articles/cls
-
web.dev - Optimize CLS https://web.dev/articles/optimize-cls
-
web.dev - Debug layout shifts https://web.dev/articles/debug-layout-shifts
-
Chrome Developers - Layout Instability API https://developer.chrome.com/docs/web-platform/layout-instability-api
-
MDN - CSS aspect-ratio https://developer.mozilla.org/en-US/docs/Web/CSS/aspect-ratio



