
CLS (Cumulative Layout Shift): Eliminating Visual Instability for a Perfect Score in 2026
Few things erode user trust faster than a page that moves beneath their fingers. A visitor reaches for a link, and just before the tap lands, an image loads above it, pushing the target downward. The click registers on an entirely different element. A form field shifts mid-keystroke. A block of text jumps as a late-loading advertisement claims its space. These experiences are not merely annoying -- they represent a fundamental failure of the interface contract between a website and the people who use it.
Cumulative Layout Shift (CLS) is the Core Web Vital that quantifies this exact class of visual instability. Unlike Largest Contentful Paint (LCP), which measures loading speed, or Interaction to Next Paint (INP), which tracks responsiveness, CLS evaluates how stable the visual presentation of a page remains throughout the user's visit. In 2026, with Google's ranking algorithms continuing to weigh page experience signals, a poor CLS score is both a user experience liability and a measurable SEO penalty.
The insidious nature of CLS is that developers on fast connections and high-end hardware rarely experience it themselves. Layout shifts are most pronounced on slower networks where resources arrive incrementally, and on lower-powered devices where rendering delays create gaps between content painting and final layout resolution. This disconnect between the development environment and real-world conditions makes CLS one of the hardest Core Web Vitals to diagnose without proper tooling and a disciplined approach to measurement.
This guide provides a complete technical breakdown of CLS: how it is calculated, what causes it, how to diagnose it, and how to systematically eliminate every source of visual instability using modern CSS, TypeScript, and Next.js.
Understanding CLS: definition and calculation
Cumulative Layout Shift is a metric that tracks the sum of all individual 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 a user interaction. The key distinction is that shifts caused by user input -- such as expanding an accordion after a click -- are excluded from the CLS calculation. Only unexpected shifts count.
Google refined the CLS calculation in 2021 with the introduction of session windows, and this model remains in effect in 2026. Rather than summing every layout shift across the entire page lifecycle (which unfairly penalized long-lived pages like single-page applications), CLS now reports the maximum session window score. A session window is a burst of layout shifts where each shift occurs within one second of the previous one, and the total window duration does not exceed five seconds. The final CLS value is the largest such window observed during the page visit.
The CLS formula
Each individual layout shift is scored using a straightforward formula:
Layout Shift Score = Impact Fraction x Distance Fraction
The resulting score is unitless and ranges from 0 (no shift) to a theoretical maximum of 1 (the entire viewport shifted by the full viewport height). In practice, individual shift scores are typically small fractions, but they accumulate rapidly when multiple elements shift simultaneously or in quick succession.
Impact fraction and distance fraction
The impact fraction measures how much of the viewport is affected by the unstable element. It is calculated as the union of the element's visible area in both the previous frame and the current frame, divided by the total viewport area. If an element occupies 25% of the viewport and shifts downward so that its new position covers an additional 10% of previously unoccupied viewport space, the impact fraction would be 0.35 (the combined area of both positions).
The distance fraction measures how far the element moved relative to the viewport. It is the greatest distance any unstable element has moved (horizontally or vertically), divided by the viewport's largest dimension (width or height). If an element shifts 150 pixels downward in a viewport that is 800 pixels tall, the distance fraction is 150 / 800 = 0.1875.
Consider a concrete example: a hero image placeholder collapses, pushing a text block that occupies 40% of the viewport downward by 200 pixels in a 900px tall viewport.
Impact Fraction = 0.40 + (200 / 900) = 0.622
Distance Fraction = 200 / 900 = 0.222
Layout Shift Score = 0.622 x 0.222 = 0.138
A single shift scoring 0.138 already pushes the page dangerously close to failing the CLS threshold on its own.
Google's thresholds
Google evaluates CLS at the 75th percentile of all page loads, using real-world data from the Chrome User Experience Report (CrUX):
- Good: CLS at or below 0.1. The page feels visually stable. Users can interact with confidence.
- Needs Improvement: CLS between 0.1 and 0.25. Noticeable shifts occur, typically during the initial load phase.
- Poor: CLS above 0.25. The page exhibits significant visual instability that disrupts reading, navigation, or form interaction.
The main causes of CLS
Layout shifts do not appear randomly. They follow predictable patterns rooted in how browsers render content progressively. Understanding these patterns is the first step toward eliminating them.
Images and media without dimensions
This is the single most common source of CLS on the web. When an <img> tag or <video> element lacks explicit width and height attributes, the browser cannot reserve the correct amount of space during the layout phase. The element initially renders as a zero-height box. When the resource finally loads, the browser recalculates the layout, and every element below the image is pushed downward.
The root cause is straightforward: the browser needs to know the aspect ratio of the media before the file downloads. Without dimensions, it has no information to work with until the first bytes of the image header arrive.
<!-- BAD: No dimensions, guaranteed CLS -->
<img src="/hero-banner.webp" alt="Product showcase" />
<!-- GOOD: Explicit dimensions allow the browser to reserve space -->
<img src="/hero-banner.webp" alt="Product showcase" width="1200" height="630" />Modern browsers use the width and height attributes to calculate the default aspect ratio before any CSS is applied. Combined with width: 100%; height: auto; in CSS, this creates a responsive image that always reserves the correct vertical space, regardless of the container width.
Web fonts and FOIT/FOUT
Custom web fonts introduce two distinct classes of layout shift. FOIT (Flash of Invisible Text) occurs when the browser hides text entirely until the custom font loads, then suddenly renders it -- potentially with different metrics than the fallback. FOUT (Flash of Unstyled Text) occurs when the browser initially renders text in a fallback system font, then swaps to the custom font once it downloads.
The layout shift happens because the custom font and the fallback font almost never share the same glyph metrics. Character widths, line heights, ascenders, and descenders differ between typefaces. When the swap occurs, lines of text reflow, paragraphs change height, and every element below the text block shifts position.
/* This triggers FOUT, which causes CLS if metrics differ */
@font-face {
font-family: 'BrandSans';
src: url('/fonts/brand-sans.woff2') format('woff2');
font-display: swap;
}Dynamically injected content
Any content that is inserted into the DOM after the initial render without space being pre-allocated will cause a layout shift. Common offenders include:
- Cookie consent banners that push page content downward when they appear
- Notification bars and promotional strips injected at the top of the page
- Lazy-loaded content that expands from zero height when it enters the viewport
- Client-side rendered components that hydrate and produce different HTML than the server-rendered shell
The pattern is always the same: an element with zero initial height suddenly acquires height, displacing everything beneath it. The severity of the shift depends on where in the document the injection occurs -- content injected at the top of the page has a far greater impact on CLS than content injected near the bottom, because more elements are displaced.
Ads and iframes
Advertising is one of the most persistent sources of CLS on the web, and one of the most difficult to control. Ad networks typically inject content via iframes with unpredictable dimensions. The ad creative may resize itself after loading, or the ad server may return different-sized creatives for different impressions.
The fundamental problem is that the publisher does not control the ad content. The iframe loads asynchronously, the creative renders at an arbitrary time, and the dimensions may change post-render. Without explicit space reservation in the publisher's layout, every ad impression becomes a potential layout shift.
<!-- BAD: Ad slot with no reserved space -->
<div id="ad-slot-top">
<!-- Ad script injects an iframe here with unknown dimensions -->
</div>
<!-- GOOD: Fixed-dimension container prevents CLS -->
<div id="ad-slot-top" style="min-height: 250px; min-width: 300px;">
<!-- Ad content loads within the reserved space -->
</div>Diagnosing CLS on your site
Because CLS is a cumulative, session-based metric that depends heavily on real-world loading conditions, diagnosing it requires both lab tools for controlled debugging and field data for understanding the actual user experience.
Chrome DevTools and Performance panel
The Chrome DevTools Performance panel provides the most granular view of layout shifts. To use it effectively:
- Open DevTools and navigate to the Performance tab.
- Enable Screenshots to see the visual state of the page at each frame.
- Start a recording, load the page, scroll through the content, and stop.
- In the resulting timeline, look for the Layout Shift entries in the Experience track. Each entry is marked with a purple bar, and hovering over it reveals the shift score, the affected elements, and the exact frame where the shift occurred.
The Performance panel also shows a Cumulative Layout Shift counter in the summary view. By examining the timeline, you can identify exactly which resource load or script execution triggered each shift, enabling targeted fixes.
// Programmatic CLS debugging using the Performance Observer API
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'layout-shift' && !(entry as any).hadRecentInput) {
console.log('Layout shift detected:', {
value: (entry as any).value,
sources: (entry as any).sources?.map((s: any) => ({
node: s.node,
previousRect: s.previousRect,
currentRect: s.currentRect,
})),
});
}
}
});
observer.observe({ type: 'layout-shift', buffered: true });Lighthouse and PageSpeed Insights
Lighthouse provides a lab-based CLS score during its simulated page load. While this does not capture post-load shifts (like those triggered by scrolling or delayed ads), it is invaluable for catching shifts that occur during the initial rendering phase. Lighthouse also provides a diagnostic filmstrip that visually highlights which elements shifted and by how much.
PageSpeed Insights combines Lighthouse lab data with CrUX field data. The field data section shows your real p75 CLS score, which is the number Google actually uses for ranking purposes. If your lab CLS is 0.02 but your field CLS is 0.18, the discrepancy points to shifts that only occur under real-world conditions -- typically caused by ads, third-party scripts, or slow network connections.
Web Vitals Extension and RUM
The Web Vitals Chrome Extension provides a real-time CLS readout as you browse your site. It updates the score continuously, allowing you to identify which specific user actions or scroll positions trigger shifts. This is particularly useful for catching shifts that occur below the fold or after user interaction.
For production monitoring, implementing Real User Monitoring (RUM) with the web-vitals library provides the most accurate picture of your CLS performance:
import { onCLS } from 'web-vitals/attribution';
onCLS((metric) => {
const { value, rating } = metric;
const largestSource = metric.attribution.largestShiftSource;
navigator.sendBeacon('/api/log-vitals', JSON.stringify({
metric: 'CLS',
value: value.toFixed(4),
rating,
largestShiftTarget: largestSource?.node
? largestSource.node.nodeName
: 'unknown',
path: window.location.pathname,
}));
});This data allows you to aggregate CLS by page template, identify the worst-performing pages, and pinpoint the exact DOM nodes responsible for the largest shifts in production.
Technical solutions to eliminate CLS
With the causes diagnosed, the next step is systematic remediation. Each solution targets a specific class of layout shift and can be implemented incrementally.
Reserving space for images and videos
The definitive solution for image-induced CLS is to always provide the browser with aspect ratio information before the image loads. There are three approaches, each suited to different contexts:
1. HTML width and height attributes (simplest, works everywhere):
<img
src="/product-photo.webp"
alt="Ergonomic office chair"
width="800"
height="600"
loading="lazy"
decoding="async"
style="width: 100%; height: auto;"
/>2. CSS aspect-ratio property (ideal for responsive containers):
.hero-image-container {
aspect-ratio: 16 / 9;
width: 100%;
overflow: hidden;
background-color: #1a1a1a; /* Placeholder color while loading */
}
.hero-image-container img {
width: 100%;
height: 100%;
object-fit: cover;
}3. The padding-bottom hack (legacy support for older browsers):
.responsive-media {
position: relative;
width: 100%;
padding-bottom: 56.25%; /* 16:9 ratio = 9/16 * 100 */
height: 0;
}
.responsive-media img,
.responsive-media iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}Optimizing font loading
Eliminating font-related CLS requires matching the metrics of your fallback font to your custom font as closely as possible. The @font-face descriptor set includes size-adjust, ascent-override, descent-override, and line-gap-override -- properties designed specifically for this purpose.
/* Step 1: Define the custom font */
@font-face {
font-family: 'BrandSans';
src: url('/fonts/brand-sans-variable.woff2') format('woff2');
font-display: swap;
}
/* Step 2: Create a metric-matched fallback */
@font-face {
font-family: 'BrandSans-Fallback';
src: local('Arial');
size-adjust: 105.2%;
ascent-override: 96%;
descent-override: 24%;
line-gap-override: 0%;
}
/* Step 3: Use both in the font stack */
body {
font-family: 'BrandSans', 'BrandSans-Fallback', sans-serif;
}The size-adjust property scales the fallback font's glyphs to match the average character width of the custom font. The override properties align the vertical metrics. When the custom font loads and the swap occurs, the text reflows minimally -- ideally producing zero measurable layout shift.
Tools like Fontaine and the Next.js built-in font optimization can generate these override values automatically.
Handling dynamic content
Dynamic content -- banners, notifications, cookie consents, and client-rendered blocks -- must be handled with explicit space reservation or insertion strategies that do not displace existing content.
Strategy 1: Reserve space with CSS min-height
.notification-banner-slot {
min-height: 48px; /* Match the banner's rendered height */
contain: layout; /* Prevent layout shifts from propagating */
}Strategy 2: Use CSS transform animations instead of layout-triggering properties
When content appears dynamically, animate it using transform rather than properties like height, margin, or padding. Transforms do not trigger layout recalculations and therefore do not contribute to CLS.
.toast-notification {
transform: translateY(100%);
transition: transform 0.3s ease-out;
position: fixed;
bottom: 0;
/* Fixed positioning removes the element from document flow */
}
.toast-notification.visible {
transform: translateY(0);
}Strategy 3: Use the CSS contain property to isolate layout boundaries
.dynamic-widget {
contain: layout size;
/* The browser treats this element as an independent layout boundary.
Changes inside it cannot shift elements outside it. */
min-height: 200px;
}Stabilizing ads
Ads require a dedicated strategy because their dimensions are often unpredictable. The core principle is to always define a minimum container size for every ad slot, based on the largest creative that could be served.
/* Reserve the standard IAB ad dimensions */
.ad-slot-leaderboard {
min-height: 90px;
min-width: 728px;
max-width: 100%;
contain: layout size style;
background-color: #0a0a0a;
}
.ad-slot-medium-rectangle {
min-height: 250px;
min-width: 300px;
contain: layout size style;
background-color: #0a0a0a;
}For ad slots that may collapse when no fill is returned, use a state-based approach:
function AdSlot({ slotId, minHeight }: { slotId: string; minHeight: number }) {
const [isLoaded, setIsLoaded] = useState(false);
const [isEmpty, setIsEmpty] = useState(false);
useEffect(() => {
// Initialize the ad slot and listen for load/empty events
const slot = window.adManager?.defineSlot(slotId, {
onLoad: () => setIsLoaded(true),
onEmpty: () => {
setIsEmpty(true);
setIsLoaded(true);
},
});
return () => slot?.destroy();
}, [slotId]);
if (isEmpty) return null; // Collapse only after confirmed empty
return (
<div
id={slotId}
style={{
minHeight: isLoaded ? undefined : minHeight,
contain: 'layout size style',
}}
/>
);
}The key insight is that the container should only collapse to zero height after the ad network confirms that no creative will be served. Collapsing before that confirmation causes a layout shift; keeping the reserved space until confirmation avoids it.
CLS and modern frameworks
Modern frameworks and CSS specifications provide built-in tools that dramatically simplify CLS prevention. Understanding how to use these tools correctly is the difference between fighting layout shifts reactively and preventing them architecturally.
Next.js Image component
The Next.js <Image> component is specifically designed to eliminate image-related CLS. When you provide width and height props, the component automatically generates the correct aspect-ratio styling and reserves space in the layout before the image loads.
import Image from 'next/image';
export function HeroBanner() {
return (
<section className="relative w-full">
<Image
src="/hero-banner.webp"
alt="High-performance e-commerce platform"
width={1920}
height={1080}
priority // Disables lazy loading for above-the-fold images
sizes="100vw"
className="w-full h-auto object-cover"
/>
</section>
);
}For images where the exact dimensions are unknown at build time (such as CMS-managed content), the fill prop combined with a sized parent container is the correct approach:
export function DynamicImage({ src, alt }: { src: string; alt: string }) {
return (
<div className="relative w-full" style={{ aspectRatio: '16 / 9' }}>
<Image
src={src}
alt={alt}
fill
sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
className="object-cover"
/>
</div>
);
}Next.js also provides built-in font optimization through next/font. This module automatically generates the @font-face declarations with metric overrides, preloads the font files, and applies the font-display: swap strategy -- all configured to minimize CLS:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
// next/font automatically calculates fallback metric overrides
});
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="en" className={inter.className}>
<body>{children}</body>
</html>
);
}CSS Container Queries
CSS Container Queries allow components to adapt their layout based on the size of their parent container rather than the viewport. This is directly relevant to CLS because it enables components to define stable, predictable layouts regardless of where they are placed in the page.
.card-grid {
container-type: inline-size;
container-name: card-grid;
}
@container card-grid (min-width: 600px) {
.card {
/* Two-column layout with stable dimensions */
display: grid;
grid-template-columns: 200px 1fr;
min-height: 200px;
}
}
@container card-grid (max-width: 599px) {
.card {
/* Single-column layout with stable dimensions */
display: flex;
flex-direction: column;
min-height: 300px;
}
}By defining explicit min-height values at each container breakpoint, you ensure that the card maintains a stable size regardless of how quickly its content loads. This is particularly valuable for cards that contain lazy-loaded images or dynamically fetched data.
Skeleton screens and placeholders
Skeleton screens are placeholder UI patterns that reserve the exact layout dimensions of the final content while it loads. Unlike spinners or loading indicators, skeletons match the structural shape of the incoming content, producing zero layout shift when the real content replaces them.
function ArticleCardSkeleton() {
return (
<div className="animate-pulse" style={{ contain: 'layout size' }}>
{/* Image placeholder */}
<div
className="bg-neutral-800 rounded-lg"
style={{ aspectRatio: '16 / 9', width: '100%' }}
/>
{/* Title placeholder */}
<div className="mt-4 h-6 bg-neutral-800 rounded w-3/4" />
{/* Description placeholders */}
<div className="mt-2 h-4 bg-neutral-800 rounded w-full" />
<div className="mt-1 h-4 bg-neutral-800 rounded w-5/6" />
{/* Meta placeholder */}
<div className="mt-4 h-3 bg-neutral-800 rounded w-1/3" />
</div>
);
}
function ArticleCard({ article }: { article: Article | null }) {
if (!article) return <ArticleCardSkeleton />;
return (
<div style={{ contain: 'layout size' }}>
<Image
src={article.image}
alt={article.title}
width={800}
height={450}
className="rounded-lg w-full h-auto"
/>
<h3 className="mt-4 text-xl font-semibold">{article.title}</h3>
<p className="mt-2 text-neutral-400">{article.description}</p>
<span className="mt-4 text-sm text-neutral-500">{article.date}</span>
</div>
);
}The critical detail is that the skeleton must occupy the exact same space as the loaded content. If the skeleton is 280px tall but the loaded card is 320px tall, the 40px difference produces a layout shift. Match dimensions precisely, and use contain: layout size to create an isolated layout boundary.
Measuring the business impact of a good CLS
Visual stability has a direct, measurable relationship with user behavior and business outcomes. When a page shifts unexpectedly, users lose their reading position, mis-click on elements, and develop a subconscious distrust of the interface. These micro-frustrations compound across sessions, degrading engagement metrics that directly affect revenue.
Research consistently demonstrates the correlation. Pages with a CLS below 0.1 show measurably lower bounce rates, higher scroll depths, and increased conversion rates compared to pages with poor CLS scores. The mechanism is straightforward: when users trust that the page will not move beneath them, they engage more deeply and complete more actions.
From an SEO perspective, CLS is one of the three Core Web Vitals that Google factors into its page experience ranking signal. While content relevance remains the dominant ranking factor, page experience serves as a differentiator in competitive search landscapes. Two pages with equivalent content authority will be ranked partly on their Core Web Vitals performance. A CLS of 0.02 versus a competitor's CLS of 0.22 is a meaningful advantage in those scenarios.
The business case for CLS optimization is also uniquely compelling because the fixes are often permanent and low-cost. Unlike INP optimization, which requires ongoing vigilance as new JavaScript is added, CLS fixes are largely structural: set image dimensions once, configure font loading once, reserve ad space once. The maintenance burden is minimal, while the improvement in user experience and search visibility persists indefinitely.
To quantify the impact for your specific site, implement RUM collection as described earlier, then correlate CLS scores with your conversion funnel:
import { onCLS } from 'web-vitals';
onCLS((metric) => {
// Send to your analytics platform for correlation analysis
window.analytics?.track('Web Vital', {
metric: 'CLS',
value: metric.value,
rating: metric.rating,
page: window.location.pathname,
// Include business context
sessionId: getSessionId(),
userSegment: getUserSegment(),
});
});By segmenting conversion rates by CLS rating (Good / Needs Improvement / Poor), you can calculate the exact revenue impact of layout instability on your site and build a data-driven case for prioritizing fixes.
Conclusion
Cumulative Layout Shift stands apart from other performance metrics because it measures something users feel viscerally but struggle to articulate. A page that shifts is a page that breaks trust. In 2026, with browsers providing increasingly sophisticated APIs for detecting and preventing layout instability, and frameworks like Next.js building CLS prevention directly into their core components, there is no technical excuse for a poor score.
The path to a perfect CLS is methodical: provide dimensions for every media element, match fallback font metrics to custom fonts, reserve space for all dynamically injected content, and isolate ad slots with explicit containers. Diagnose with field data from CrUX, debug with Chrome DevTools, and monitor continuously with RUM. Each fix is targeted, testable, and permanent.
The compound effect of eliminating visual instability extends beyond the metric itself. Users scroll further, click with confidence, and complete conversions without the friction of unexpected page movement. Search engines reward the stability with improved page experience signals. The investment is modest -- primarily upfront CSS and HTML hygiene -- and the returns, measured in both user satisfaction and search visibility, are sustained and significant.
