Cumulative Layout Shift (CLS) measures visual stability -- how much page content moves unexpectedly during loading. It is one of three Core Web Vitals that Google uses as a ranking signal. A poor CLS score frustrates users and signals low page quality to search engines.
Google's CLS Thresholds
| Rating | CLS Score |
|---|---|
| Good | Under 0.1 |
| Needs Improvement | 0.1 - 0.25 |
| Poor | Over 0.25 |
Measured at the 75th percentile of real user sessions. CLS accumulates across the entire page lifecycle, not just initial load.
How CLS Is Calculated
CLS equals the sum of all unexpected layout shift scores during the page session. Each individual shift score is calculated as:
Layout Shift Score = Impact Fraction x Distance Fraction
- Impact fraction: the percentage of the viewport affected by the shifting elements
- Distance fraction: how far the elements moved relative to the viewport
A large image that pushes the entire page down 300px generates a much higher shift score than a small badge moving 10px.
Top CLS Offenders and Fixes
Images and Videos Without Dimensions
The single most common CLS cause. When width and height attributes are missing, the browser allocates zero space until the image loads, then reflows the page.
<!-- Always include width and height -->
<img src="/product.webp" width="800" height="600" alt="Product photo">
<!-- Or use CSS aspect-ratio for responsive images -->
<style>
.hero-img { aspect-ratio: 16 / 9; width: 100%; height: auto; }
</style>
Web Fonts Causing Text Reflow
When a web font loads and replaces the fallback font, text blocks can change size. Use font-display: optional for the most stable behavior, or match your fallback font metrics closely:
@font-face {
font-family: 'BrandFont';
src: url('/fonts/brand.woff2') format('woff2');
font-display: optional; /* Prevents layout shift from font swap */
}
If you must use font-display: swap, preload the font file to minimize the swap window:
<link rel="preload" href="/fonts/brand.woff2" as="font" type="font/woff2" crossorigin>
Dynamic Content Injected Above Existing Content
Banners, cookie consent bars, and dynamically loaded ads that insert above visible content push everything down. Reserve space for these elements with minimum heights:
.ad-slot { min-height: 250px; } /* Reserve space for standard 300x250 ad */
.cookie-banner { min-height: 60px; }
Ads and Embeds Without Reserved Space
Third-party ad networks and embed scripts (YouTube, Twitter, etc.) load asynchronously and expand after the page renders. Always define explicit dimensions for ad containers and use CSS contain: layout to prevent them from affecting surrounding elements.
Debugging CLS in Chrome DevTools
- Open DevTools and go to the Performance tab
- Check "Screenshots" and record a page load
- Look for red-highlighted "Layout Shift" entries in the Experience lane
- Click each shift to see which elements moved and their shift scores
The Layout Shift Regions feature in DevTools Rendering tab highlights shifting areas in real time with blue overlays.
CSS Containment for Stability
The contain property tells the browser that an element's layout is independent of the rest of the page:
.widget-container {
contain: layout style;
/* Browser won't let this element's changes affect sibling layout */
}
Monitoring CLS in Production
Use the web-vitals library to capture CLS from real users and send it to your analytics platform. Pay attention to CLS values after user interaction, not just initial load, since content loaded via infinite scroll or tab switches contributes to CLS.
import {onCLS} from 'web-vitals';
onCLS(metric => sendToAnalytics('CLS', metric.value));
Check Search Console's Core Web Vitals report monthly to track the percentage of URLs with good CLS across mobile and desktop.