Complete Guide to Cumulative Layout Shift (CLS)
Cumulative Layout Shift (CLS) is the Core Web Vital that measures visual stability. It quantifies how much visible content unexpectedly moves around during the entire lifespan of a page. A good CLS score -- 0.1 or less -- means your users never experience the frustration of clicking a button only to have the page jump and hit the wrong element.
CLS is uniquely frustrating because it causes users to lose their place, misclick, or feel disoriented. Unlike slow loading (which users can wait through) or slow interactions (which feel sluggish), layout shifts actively interfere with what the user is doing. This guide covers how CLS is calculated, why it matters for both SEO and user experience, the most common causes, and detailed step-by-step fixes with real code.
What is Cumulative Layout Shift?
CLS measures the sum of all unexpected layout shift scores that occur during the entire lifespan of a page. A layout shift occurs any time a visible element changes its position from one rendered frame to the next without being triggered by user interaction.
The metric was introduced by Google as part of Core Web Vitals because existing metrics like page load time failed to capture the jarring experience of content jumping around. You might have a page that loads in under a second but delivers a terrible experience because an ad slot injects itself between the headline and the first paragraph after the text has already rendered.
How CLS is calculated
Every individual layout shift gets a score computed from two factors:
- Impact fraction: The total area of the viewport affected by the shift. If an element occupies 30% of the viewport and shifts down, pushing content in another 20% of the viewport, the impact fraction is 0.5 (50%).
- Distance fraction: The greatest distance any element has moved, divided by the viewport's largest dimension. If an element shifts down by 25% of the viewport height, the distance fraction is 0.25.
The shift score for a single frame is: impact fraction x distance fraction. So: 0.5 x 0.25 = 0.125.
CLS uses a session window approach to calculate the final score. A session window is a group of layout shifts that occur within 1 second of each other, with a maximum window duration of 5 seconds. The CLS score reported is the maximum session window score -- not the sum of all shifts across the entire page lifetime. This windowed approach replaced the original "sum of all shifts" method in 2021 and is significantly fairer to long-lived pages like SPAs.
// Measure CLS using the web-vitals library
import {onCLS} from 'web-vitals';
onCLS(({value, entries}) => {
console.log('CLS:', value);
// Log each individual shift for debugging
for (const entry of entries) {
console.log('Shift:', {
value: entry.value,
sources: entry.sources?.map(s => ({
node: s.node,
previousRect: s.previousRect,
currentRect: s.currentRect,
})),
});
}
});
CLS thresholds
Google categorizes CLS scores into three buckets:
Like all Core Web Vitals, Google evaluates CLS at the 75th percentile of page loads. This means 75% of your real-world page visits need a CLS at or below 0.1 to receive a "Good" rating in the Chrome User Experience Report (CrUX).
Why CLS matters for SEO and UX
CLS directly impacts two things search engines and users care about:
1. User trust and engagement. Layout shifts break the implicit contract between a website and its user. When a user reads text and the page jumps, or when they aim for a button and the target moves, it erodes trust. Research from Google shows that layout instability is the second most-cited complaint from mobile web users, behind only slow loading.
2. Search ranking signals. CLS is one of Google's three Core Web Vitals and a direct ranking factor. In the December 2025 core update, Google increased the weight of user experience signals across all search verticals. Sites with consistently poor CLS are at a measurable ranking disadvantage, especially in competitive niches where content quality is similar across competitors.
The e-commerce impact is particularly acute. A layout shift that causes a user to add the wrong item to their cart, or accidentally dismiss a checkout flow, creates immediate revenue loss. Studies show that reducing CLS from 0.25 to under 0.1 can decrease cart abandonment by up to 8%.
Common causes of poor CLS
Layout shifts stem from a handful of predictable patterns. Understanding these patterns makes diagnosis straightforward.
1. Images and videos without dimensions
This is the single most common cause of CLS on the web. When an <img> or <video> element lacks width and height attributes, the browser initially renders it at 0x0 pixels. When the resource loads and the browser knows the actual dimensions, the element expands, pushing all content below it down the page. On a typical blog post with a hero image, this single issue can produce a CLS score of 0.3 or higher.
2. Dynamically injected content
Ads, cookie consent banners, promotional bars, and any DOM element injected above existing content without pre-allocated space will cause layout shifts. Ad slots are especially problematic because their final dimensions often depend on the winning bid in a real-time auction, which means you don't know the exact height in advance.
3. Web fonts causing text reflow
When a web font loads and replaces the fallback system font, the text often reflows because the two fonts have different metrics (x-height, character width, line height). This is known as FOUT (Flash of Unstyled Text) when the fallback renders first, or FOIT (Flash of Invisible Text) when the browser hides text until the web font arrives. Both patterns cause layout shifts: FOUT when the reflow happens, and FOIT when invisible text suddenly appears and takes up space.
4. Late-loading CSS or JavaScript resizing the DOM
Stylesheets or scripts that load asynchronously and then change the size or position of visible elements cause shifts. Common examples: a non-critical CSS file that changes a container's padding, a JavaScript widget that calculates and applies a specific height, or a third-party embed that resizes its container after initialization.
5. Non-composited animations
CSS animations or transitions that change layout-triggering properties (width, height, top, left, margin, padding) instead of compositor-friendly properties (transform, opacity) can cause layout shifts. A sliding sidebar that animates margin-left will trigger shifts on every frame; the same animation using transform: translateX() will not.
Step-by-step CLS optimization
Follow these steps in order of impact. Most sites will achieve a CLS under 0.1 with just the first three steps.
Set explicit dimensions on all media
Always include width and height attributes on <img> and <video> elements. Modern browsers use these to calculate the aspect ratio before the image loads, reserving the correct space. For responsive images, the CSS aspect-ratio property or the width/height attribute pair combined with width: 100%; height: auto; ensures proper space reservation at any viewport size.
<!-- Always set width and height -->
<img src="/hero.webp"
width="1200" height="630"
alt="Dashboard showing CLS metrics"
loading="lazy"
decoding="async">
<!-- For responsive images, combine with CSS -->
<style>
img { max-width: 100%; height: auto; }
</style>
<!-- Or use the aspect-ratio property -->
<style>
.hero-img { aspect-ratio: 16 / 9; width: 100%; object-fit: cover; }
</style>
Reserve space for dynamic content
For ad slots, embeds, iframes, and any dynamically loaded content, reserve a fixed space using min-height or a container with aspect-ratio. If you don't know the exact dimensions, estimate conservatively -- a slightly too-tall placeholder is far better than no placeholder at all.
/* Reserve space for ad slots */
.ad-slot {
min-height: 250px; /* Common ad height */
background: var(--color-surface);
display: flex;
align-items: center;
justify-content: center;
}
.ad-slot::before {
content: 'Advertisement';
font-size: 0.75rem;
color: var(--color-text-muted);
}
/* Reserve space for embeds with known aspect ratio */
.embed-container {
aspect-ratio: 16 / 9;
width: 100%;
overflow: hidden;
background: var(--color-surface);
}
/* Reserve space for cookie banner at bottom */
.cookie-banner {
position: fixed;
bottom: 0;
/* Fixed positioning does NOT cause layout shifts */
}
Optimize web font loading
Use font-display: swap with a size-adjusted fallback font to minimize the layout reflow when the web font loads. The size-adjust, ascent-override, and descent-override descriptors in @font-face let you match the fallback font metrics to your web font, eliminating or minimizing the text reflow. Tools like Fallback Font Generator can calculate these values automatically.
/* Main web font */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-var.woff2') format('woff2');
font-display: swap;
font-weight: 100 900;
}
/* Size-adjusted fallback to prevent CLS */
@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', system-ui, sans-serif;
}
Avoid inserting content above existing content
Never dynamically insert banners, notifications, or promotional content above the fold in a way that pushes existing content down. Instead, use fixed or sticky positioning, or render the placeholder server-side. If you must insert content, use CSS content-visibility and contain-intrinsic-size to contain the layout impact.
// BAD: Inserting a banner above existing content
const banner = document.createElement('div');
banner.textContent = 'Special offer!';
document.body.prepend(banner); // Pushes everything down = CLS
// GOOD: Use fixed/sticky positioning
const banner = document.createElement('div');
banner.textContent = 'Special offer!';
banner.style.position = 'sticky';
banner.style.top = '0';
banner.style.zIndex = '100';
document.body.prepend(banner); // No CLS - fixed position
// GOOD: Reserve the space server-side
// Render the banner container in your HTML template
// even if it's empty, with a fixed min-height
Use transform for animations
Replace any animation that changes layout properties with transform and opacity. These properties are handled by the compositor thread and do not trigger layout recalculation, which means they cannot cause layout shifts.
/* BAD: Animating layout properties */
.sidebar {
transition: margin-left 0.3s ease;
}
.sidebar.open {
margin-left: 0; /* Triggers layout shift */
}
/* GOOD: Use transform instead */
.sidebar {
transform: translateX(-100%);
transition: transform 0.3s ease;
}
.sidebar.open {
transform: translateX(0); /* No layout shift */
}
Advanced CLS optimization techniques
content-visibility for off-screen content
The CSS content-visibility: auto property tells the browser to skip rendering off-screen content until it's needed. When paired with contain-intrinsic-size, you can specify an estimated size for the skipped content so the scrollbar and page length remain stable. This prevents layout shifts from content that loads lazily below the fold.
/* Skip rendering off-screen sections */
.article-section {
content-visibility: auto;
contain-intrinsic-size: auto 500px;
/* auto = use cached size after first render */
/* 500px = estimated height before first render */
}
View Transitions API
The View Transitions API (available in Chrome 111+) provides a browser-native way to animate between page states without causing layout shifts. Instead of directly manipulating the DOM and triggering shifts, you wrap your DOM changes in document.startViewTransition() and the browser captures snapshots and animates between them using compositor-friendly transforms.
Back/Forward Cache (bfcache) and CLS
When a user navigates back to a page using the browser back button, bfcache restores the page from memory with zero layout shifts. However, if your page runs JavaScript on the pageshow event that modifies the DOM, you can introduce shifts on the restored page. Audit your pageshow and visibilitychange handlers to ensure they don't trigger layout changes.
Measuring CLS: tools and techniques
CLS measurement requires careful attention because it captures shifts throughout the page lifetime, not just during initial load.
Lab tools (Lighthouse, DevTools) only measure CLS during page load simulation and may miss shifts that occur during scrolling, after clicking, or from lazy-loaded content. Lab CLS scores are often artificially low compared to field data.
Field tools (CrUX, web-vitals library, RUM providers) capture the full user experience including scroll-triggered shifts and interaction-driven shifts. This is what Google uses for ranking.
Debugging workflow:
- Check your CLS in CrUX (via PageSpeed Insights or Search Console) to see real-user scores
- Open Chrome DevTools, enable "Layout Shift Regions" in the Rendering tab to see shifts highlighted in blue
- Use the Performance panel to record a full page session (load, scroll, interact) and look at the Experience row for shift markers
- Use the
web-vitalslibrary withattributionbuild to log shift sources to your analytics
// Detailed CLS debugging with attribution
import {onCLS} from 'web-vitals/attribution';
onCLS(({value, attribution}) => {
console.log('CLS:', value);
console.log('Largest shift target:',
attribution.largestShiftTarget);
console.log('Largest shift time:',
attribution.largestShiftTime);
console.log('Largest shift value:',
attribution.largestShiftValue);
// Send to analytics for field monitoring
sendToAnalytics({
metric: 'CLS',
value,
target: attribution.largestShiftTarget,
});
});
Framework-specific CLS fixes
Each framework has unique CLS patterns:
Next.js: The <Image> component automatically prevents CLS by enforcing width/height and generating responsive sizes. Use next/font for automatic font optimization with font-display: swap and size-adjusted fallbacks. Watch out for client-side hydration mismatches that can cause unexpected DOM changes.
React: Use Suspense boundaries with fallback UIs that match the expected dimensions of the loaded content. Avoid useEffect patterns that modify DOM dimensions on the first render -- this creates a shift between the SSR output and the hydrated client state.
WordPress: The most common CLS source in WordPress is images without dimensions. WordPress 5.5+ adds width/height automatically, but themes and plugins may override this. Use the wp_get_attachment_image() function which includes dimensions by default. For ad plugins, ensure ad containers have reserved heights.
Vue / Nuxt: Nuxt's <NuxtImg> component handles dimensions automatically. For dynamic content loaded with useFetch or useAsyncData, ensure the loading state placeholder matches the final content dimensions.
Frequently asked questions
A good CLS score is 0.1 or less. Scores between 0.1 and 0.25 need improvement, and anything above 0.25 is considered poor. Google uses the 75th percentile of page loads, meaning 75% of your real-world visits need CLS at or below 0.1 for a "Good" rating in CrUX and Search Console.
The five most common causes are: (1) images and videos without width/height attributes, (2) dynamically injected content like ads and banners, (3) web fonts causing text reflow (FOIT/FOUT), (4) late-loading CSS or JavaScript that resizes DOM elements, and (5) CSS animations that use layout-triggering properties instead of transforms.
Each layout shift score = impact fraction (% of viewport affected) multiplied by distance fraction (how far elements moved relative to viewport). CLS uses a "session window" approach: shifts within 1 second of each other are grouped into windows (max 5s each), and the largest window score is reported as CLS. This is fairer to long-lived pages like SPAs.
No. CLS measures layout shifts throughout the entire lifespan of the page. This includes shifts from lazy-loaded images that appear on scroll, infinite scroll content injection, API data that loads after user interaction, and any other DOM change that causes visible elements to move. This is why lab tools (which only measure during load) often report lower CLS than field data.
No. Layout shifts that occur within 500ms of a user interaction (click, tap, or keypress) are excluded from CLS. This means accordions, dropdown menus, expandable sections, and toggled content do not penalize your CLS score -- as long as the DOM change happens within that 500ms window. Scroll is not considered a user input for this purpose, so scroll-triggered animations can still cause CLS.
Three approaches: (1) Open DevTools > Rendering drawer > enable "Layout Shift Regions" for real-time blue highlights. (2) Use the Performance panel to record a session and inspect "Layout Shift" entries in the Experience row -- click each one to see the shifting element. (3) Use the Console snippet new PerformanceObserver(list => list.getEntries().forEach(e => console.log(e))).observe({type: 'layout-shift', buffered: true}) to log all shifts with their source elements.
When a browser downloads a custom font, it swaps from a fallback font to the custom font, causing text to reflow because the fonts have different metrics (character widths, line heights). This typically causes 0.05-0.15 CLS per font swap. The fix is using font-display: swap combined with size-adjust metric overrides so the fallback font matches the custom font dimensions. See our font loading CLS guide for implementation details.
Chrome DevTools Layout Shift Regions (under Rendering tab) visually highlights elements that shift. The Performance panel shows each layout shift event with its contribution score. The web-vitals library with attribution build identifies the exact shifting element programmatically. For production monitoring, set up real-user monitoring to catch CLS issues that only appear in the field.
Fix CLS in your framework
Step-by-step optimization guides for every major framework and platform:
Fix CLS in Next.js
Image dimensions, font loading, and dynamic content stability in Next.js.
FixFix CLS in React
Layout stability patterns, Suspense boundaries, and skeleton screens.
FixFix CLS in Vue
Transition handling, async component layouts, and CSS containment in Vue.
FixFix CLS in Angular
ViewEncapsulation, layout directives, and CDK overlay management.
FixFix CLS in WordPress
Image dimensions, font preloading, and plugin audit for layout stability.
FixFix CLS with Tailwind CSS
aspect-ratio utilities, min-height skeletons, intrinsic image sizing, and font-display fixes for utility-first sites.
FixFix CLS with Bootstrap
Ratio helpers, scrollbar-gutter stable, validation message space reservation, and Bootstrap Icons font-display swap.
GuideFont Loading Best Practices
font-display swap, preloading, subsetting, variable fonts, and size-adjust fallbacks to eliminate font-driven layout shift.
FixFix CLS in Shopify
App-injected content, variant selectors, and announcement bar stability.
FixFix CLS in Webflow
Interaction animations, embed stability, and responsive layout fixes.
GuideLCP Guide
Largest Contentful Paint optimization -- often improved alongside CLS fixes.
ComparisonReact vs Vue: Web Vitals
Framework comparison with CLS data from 50K production origins.