Understanding CLS: The Complete Guide to Cumulative Layout Shift

Understanding CLS: The Complete Guide to Cumulative Layout Shift

Master Cumulative Layout Shift (CLS), the Core Web Vital that measures visual stability. Learn about CLS history, how it's calculated, why layout shifts frustrate users, and proven strategies to achieve a perfect CLS score.

Understanding CLS: The Complete Guide to Cumulative Layout Shift

What is Cumulative Layout Shift (CLS)?

Cumulative Layout Shift (CLS) is a Core Web Vital that measures visual stability: how much the content on a page moves unexpectedly while it’s loading and during user interaction. It quantifies the frustrating experience of trying to click a button only to have it move at the last moment, or losing your place while reading because text suddenly shifted.

Unlike the other Core Web Vitals that measure time, CLS is a unitless score representing the total amount of unexpected layout shifting. The lower the score, the more stable your page.

The Frustration of Layout Shifts

We’ve all experienced it: you’re about to tap a link on your phone when suddenly an ad loads above it, and you accidentally click the ad instead. Or you’re reading an article when an image loads and pushes the text down, losing your place.

These aren’t just annoyances. They can lead to:

  • Accidental clicks that navigate away from the page
  • Lost reading context
  • Form submission errors
  • General frustration and loss of trust

CLS measures these experiences objectively.

CLS Thresholds

RatingScore
Good≤ 0.1
Needs Improvement0.1 - 0.25
Poor> 0.25

To provide a good experience, Google recommends that the 75th percentile of page visits have a CLS of 0.1 or less.

How CLS is Calculated

The Layout Shift Score Formula

Each individual layout shift is scored using this formula:

Layout Shift Score = Impact Fraction × Distance Fraction

Impact Fraction: The percentage of the viewport affected by the shift. If an element takes up 50% of the viewport and shifts, the impact fraction is at least 0.5.

Distance Fraction: How far the element moved, as a percentage of the viewport. If an element moves 100 pixels in a 800-pixel viewport, the distance fraction is 0.125 (100/800).

Example Calculation

Consider a hero image that:

  • Takes up 50% of the viewport height
  • Shifts down by 10% of the viewport height

Impact Fraction: The image’s original position plus its new position spans 60% of the viewport = 0.6

Distance Fraction: The image moved 10% of the viewport = 0.1

Layout Shift Score: 0.6 × 0.1 = 0.06

If this was the only shift, your CLS would be 0.06, which is a good score.

What Counts as a Layout Shift?

Included:

  • Elements that move position in the viewport
  • Elements that appear and push other content
  • Size changes that affect layout

Excluded (Expected Shifts):

  • Shifts caused by user interaction (within 500ms of input)
  • Elements with position: fixed or position: sticky
  • Transitions within iframes (counted separately)
  • Scroll-driven changes

The Session Window Approach

Originally, CLS accumulated all layout shifts throughout the page’s entire lifecycle. This caused problems for long-lived pages (like infinite scroll feeds or single-page apps) where CLS could grow indefinitely.

In 2021, Google changed CLS to use “session windows”:

  1. Session windows are groups of layout shifts occurring within 1 second of each other
  2. Gaps between windows must be at least 1 second
  3. Maximum window duration is 5 seconds
  4. CLS = the largest session window’s total score

This means your CLS represents the worst “burst” of layout shifts, not the sum of everything.

The History of CLS

Before CLS: No Standard Metric

Before CLS, there was no standardized way to measure visual stability. Developers knew layout shifts were bad, but:

  • There was no objective measurement
  • Different tools measured it differently
  • It was hard to prioritize fixes without data

May 2020: CLS Introduced

Google introduced CLS as part of Core Web Vitals alongside LCP and FID. The initial implementation used a simple cumulative approach where all shifts added up throughout the page lifecycle.

Initial Thresholds:

  • Good: ≤ 0.1
  • Needs Improvement: 0.1 - 0.25
  • Poor: > 0.25

Early Challenges (2020-2021)

The original cumulative approach caused issues:

Long-Lived Pages Problem News sites, social feeds, and SPAs accumulated shifts over time. A page that was fine for 30 seconds might have a poor CLS after 5 minutes.

Carousel Problem Image carousels that auto-advanced accumulated shifts even though the behavior was intentional.

Infinite Scroll Problem Loading more content in infinite scroll pages caused continuous CLS accumulation.

June 2021: Session Windows Update

Google redesigned CLS measurement to use session windows, addressing the long-lived pages problem. This was one of the most significant changes to a Core Web Vital since launch.

Key Changes:

  • Shifted from lifetime cumulative to “worst burst” measurement
  • Added the 1-second gap and 5-second maximum rules
  • Made CLS more stable for complex pages

2022-2023: Refinements

Google continued refining CLS:

  • Better handling of bfcache (back/forward cache) navigations
  • Improved SPA support
  • More accurate attribution data for debugging
  • Better iframe handling

2024-2025: Maturity

CLS measurement has stabilized. Focus shifted to:

  • Better tooling and debugging
  • Documentation improvements
  • Framework-level solutions

Why CLS Matters

User Trust and Experience

Layout shifts erode trust. Users subconsciously interpret instability as:

  • The site is broken or poorly made
  • The content might change unexpectedly
  • They can’t trust their interactions

High CLS sites feel “janky” and unreliable.

Business Impact

Research demonstrates CLS’s effect on business metrics:

Yahoo! JAPAN: Reduced CLS by 0.2 and saw a 15% increase in page views per session.

Economic Times: Improved CLS from 0.25 to under 0.1 and reduced bounce rate by 43%.

Vodafone: Along with other Core Web Vitals improvements, CLS optimization contributed to an 8% increase in sales.

Accidental Clicks and Lost Conversions

One of the most direct impacts of poor CLS is accidental clicks:

  • Users click ads they didn’t intend to
  • Form submissions go wrong
  • Navigation becomes unpredictable

These create frustration and can directly cost conversions.

SEO Ranking Signal

CLS is part of Core Web Vitals, which influences Google rankings. A good CLS score won’t guarantee top rankings, but poor CLS can hurt your competitive position.

Common Causes of Poor CLS

1. Images Without Dimensions

The most common cause of CLS. When images load without width and height attributes, the browser can’t reserve space for them.

Symptoms:

  • Content jumps when images load
  • Large shift scores from hero images
  • Layout shifts scattered throughout scroll

Solutions:

<!-- Always include width and height -->
<img src="hero.jpg" width="800" height="400" alt="Hero image">

<!-- Modern approach with aspect ratio -->
<img src="hero.jpg" style="aspect-ratio: 2/1; width: 100%;" alt="Hero image">

2. Ads, Embeds, and Dynamic Content

Third-party content injected into the page often causes shifts because space isn’t reserved for it.

Symptoms:

  • Sudden shifts when ads load
  • Content pushed down by social embeds
  • Chat widgets appearing and pushing content

Solutions:

  • Reserve space with fixed-height containers
  • Use skeleton screens
  • Load ads below the fold initially
  • Use sticky positioning for chat widgets

3. Web Fonts Causing FOIT/FOUT

When web fonts load, text can reflow:

  • FOIT (Flash of Invisible Text): Text is hidden until font loads, then appears
  • FOUT (Flash of Unstyled Text): Text displays in fallback font, then shifts to web font

Symptoms:

  • Text blocks shift when fonts load
  • Line heights change
  • Elements resize based on text width

Solutions:

/* Use font-display: swap or optional */
@font-face {
  font-family: 'Custom Font';
  src: url('/fonts/custom.woff2') format('woff2');
  font-display: swap; /* or optional */
}

/* Match fallback font metrics */
@font-face {
  font-family: 'Custom Font Fallback';
  src: local('Arial');
  ascent-override: 90%;
  descent-override: 20%;
  line-gap-override: 0%;
}

4. Dynamically Injected Content

JavaScript that inserts content above existing content causes shifts.

Symptoms:

  • Banners appearing at the top
  • Notifications pushing content
  • New items added to the top of lists

Solutions:

  • Insert content below existing content
  • Use fixed-position overlays instead of inline elements
  • Reserve space for dynamic content
  • Use CSS transforms to animate in without causing layout shifts

5. Animations That Affect Layout

CSS animations that change layout properties (width, height, top, left, margin, padding) cause layout shifts.

Symptoms:

  • Elements expanding/collapsing with animation
  • Hover effects that change element size
  • Slide-in animations using positional properties

Solutions:

/* Bad: Animates layout properties */
.expanding {
  transition: height 0.3s;
}

/* Good: Uses transform and opacity */
.sliding-in {
  transition: transform 0.3s, opacity 0.3s;
}
.sliding-in.visible {
  transform: translateY(0);
  opacity: 1;
}
.sliding-in.hidden {
  transform: translateY(-100%);
  opacity: 0;
}

6. Slow-Loading Components

Components that render late and push content down.

Symptoms:

  • SSR content followed by client hydration shifts
  • Lazy-loaded components appearing and shifting layout
  • Cookie consent banners loading late

Solutions:

  • Use skeleton screens with accurate dimensions
  • Inline critical component CSS
  • Prerender or SSR dynamic components
  • Position banners as overlays

Optimizing CLS: Proven Strategies

Always Set Image Dimensions

<!-- HTML approach -->
<img src="image.jpg" width="600" height="400" alt="Description">

<!-- CSS approach -->
<style>
img {
  aspect-ratio: 3 / 2;
  width: 100%;
  height: auto;
}
</style>

Reserve Space for Ads

/* Create a container with fixed dimensions for ads */
.ad-slot {
  min-height: 250px;
  width: 300px;
  background: #f0f0f0;
  display: flex;
  align-items: center;
  justify-content: center;
}

.ad-slot::before {
  content: "Advertisement";
  color: #999;
}

Optimize Font Loading

<!-- Preload critical fonts -->
<link rel="preload" as="font" type="font/woff2"
      href="/fonts/main.woff2" crossorigin>

<style>
/* Use font-display */
@font-face {
  font-family: 'Main Font';
  src: url('/fonts/main.woff2') format('woff2');
  font-display: optional; /* Prevents FOUT entirely */
}

/* Or use swap with matched fallback */
@font-face {
  font-family: 'Main Font';
  src: url('/fonts/main.woff2') format('woff2');
  font-display: swap;
  size-adjust: 105%; /* Match metrics */
}
</style>

Use CSS Transforms for Animations

/* Animate with transform instead of top/left */
.notification {
  position: fixed;
  top: 20px;
  right: 20px;
  transform: translateX(120%);
  transition: transform 0.3s ease-out;
}

.notification.visible {
  transform: translateX(0);
}

Implement Skeleton Screens

<div class="content-card">
  <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;
}

.skeleton-text {
  height: 1em;
  margin: 0.5em 0;
  background: #f0f0f0;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}
</style>

Position Dynamic Content Strategically

<!-- Bad: Banner at top pushes content down -->
<body>
  <div class="dynamic-banner">...</div>
  <main>...</main>
</body>

<!-- Good: Banner as overlay -->
<body>
  <main>...</main>
  <div class="banner-overlay">...</div>
</body>

<style>
.banner-overlay {
  position: fixed;
  bottom: 0;
  left: 0;
  right: 0;
  transform: translateY(100%);
  transition: transform 0.3s;
}

.banner-overlay.visible {
  transform: translateY(0);
}
</style>

Measuring and Debugging CLS

Tools for CLS Analysis

Chrome DevTools

  1. Performance panel: Record page load
  2. Look for “Layout Shift” entries
  3. Experience track shows shift locations

PageSpeed Insights

  • Shows CLS in lab and field data
  • Highlights elements causing shifts
  • Provides specific recommendations

Web Vitals Extension

  • Real-time CLS monitoring
  • Shows each individual shift
  • Highlights shifted elements visually

Layout Shift Debugger (DevTools)

  1. Open DevTools
  2. Press Ctrl+Shift+P
  3. Type “Show Layout Shift Regions”
  4. Blue boxes highlight areas that shift

Using the web-vitals Library

import {onCLS} from 'web-vitals';

onCLS(metric => {
  console.log('CLS:', metric.value);
  console.log('Entries:', metric.entries);

  // See which elements shifted
  metric.entries.forEach(entry => {
    entry.sources?.forEach(source => {
      console.log('Shifted element:', source.node);
    });
  });

  // Send to analytics
  sendToAnalytics({
    name: 'CLS',
    value: metric.value,
    rating: metric.rating,
    entries: metric.entries.length,
  });
});

Identifying Shifted Elements

In Chrome DevTools:

  1. Open Performance panel
  2. Record a page load
  3. Find “Layout Shift” entries in the timeline
  4. Click an entry to see which elements shifted
  5. The elements are highlighted in the viewport

CLS Debugging Checklist

  • Check all images have width and height attributes
  • Verify fonts use font-display: swap or optional
  • Ensure ad slots have reserved dimensions
  • Test with slow network to simulate late-loading content
  • Check for dynamically injected content above the fold
  • Verify animations use transforms, not layout properties
  • Test on mobile devices (different viewport = different shifts)
  • Use DevTools “Layout Shift Regions” to visualize shifts
  • Check for lazy-loaded content that shifts layout

Framework-Specific Solutions

React

// Use placeholder/skeleton while loading
function ImageWithPlaceholder({ src, width, height, alt }) {
  const [loaded, setLoaded] = useState(false);

  return (
    <div style={{ aspectRatio: `${width}/${height}`, position: 'relative' }}>
      {!loaded && <div className="skeleton" />}
      <img
        src={src}
        width={width}
        height={height}
        alt={alt}
        onLoad={() => setLoaded(true)}
        style={{ opacity: loaded ? 1 : 0 }}
      />
    </div>
  );
}

Next.js

// Next.js Image component handles CLS automatically
import Image from 'next/image';

<Image
  src="/hero.jpg"
  width={800}
  height={400}
  alt="Hero image"
  priority // For LCP images
/>

Vue

<template>
  <div :style="{ aspectRatio: `${width}/${height}` }">
    <img
      :src="src"
      :width="width"
      :height="height"
      :alt="alt"
      @load="loaded = true"
    />
  </div>
</template>

Common CLS Mistakes to Avoid

1. Using auto for Image Dimensions

<!-- Bad: No explicit dimensions -->
<img src="photo.jpg" style="width: auto; height: auto;">

<!-- Good: Explicit dimensions with responsive sizing -->
<img src="photo.jpg" width="800" height="600" style="max-width: 100%; height: auto;">

2. Injecting Content at the Top of the Page

Don’t add banners, notifications, or alerts that push content down. Use overlays or insert at the bottom.

3. Using Layout-Affecting CSS for Animations

Properties like width, height, padding, margin, top, left trigger layout. Use transform and opacity instead.

4. Forgetting About Font Loading

Even small font metric differences cause shifts. Use font-display, preload fonts, and match fallback metrics.

5. Testing Only on Fast Connections

Layout shifts often occur on slow connections when resources load asynchronously. Test with throttled networks.

Conclusion

Cumulative Layout Shift measures something we all intuitively understand: pages shouldn’t jump around unexpectedly. By quantifying visual stability, CLS gives us a concrete target to optimize for and helps us build pages that feel solid and trustworthy.

The key to good CLS is planning ahead: reserve space for images before they load, use transforms instead of layout properties for animations, and never inject content that pushes other content around.

A stable page isn’t just about hitting a metric threshold. It’s about respecting your users’ attention and creating an experience where they can focus on your content instead of fighting with your layout.

FAQs

1. What’s a good CLS score? 0.1 or less is “good.” Between 0.1-0.25 needs improvement, and over 0.25 is poor.

2. Why does CLS measure a score instead of time? CLS measures the amount of movement, not how long it takes. A small shift is less disruptive than a large one, regardless of timing.

3. Do user-triggered shifts count toward CLS? No. Shifts within 500ms of user interaction (click, tap, keypress) are excluded. Users expect feedback from their actions.

4. Why is my CLS different on mobile vs. desktop? Different viewport sizes mean different impact and distance fractions. Also, resources may load in different orders on different connection speeds.

5. How do I find which elements are causing CLS? Use Chrome DevTools Performance panel, the Web Vitals extension, or the web-vitals library with attribution. These show exactly which elements shifted.

6. Does scrolling cause CLS? No. Content moving because of user scrolling doesn’t count as a layout shift. Only unexpected shifts while the user isn’t interacting are counted.

7. What about infinite scroll pages? The session window approach means only the worst “burst” of shifts counts. Loading more content at the bottom during scroll doesn’t typically cause CLS issues since it’s below the viewport.

// SYS.FOOTER