Cumulative Layout Shift: what it measures and how to fix it
CLS scores how much a page jumps while people use it. Good is 0.1 or less at the 75th percentile. The common causes, how to measure them, and fixes.
8 min read
Cumulative Layout Shift (CLS) measures how much visible content moves unexpectedly while a page is open. A score of 0.1 or less is good, up to 0.25 needs improvement, and above 0.25 is poor, judged at the 75th percentile of page loads on mobile and desktop. Most shifts come from images without dimensions, ads and embeds, late web fonts and injected banners. The fix is almost always the same: reserve the space before the content arrives.
You go to tap a link and the page drops by a few hundred pixels. You hit an advert instead. web.dev gives a worse example: a shift that makes someone confirm a large order they meant to cancel. A button can be perfectly sized for a thumb and still get missed if it moves at the wrong moment. This post explains how CLS is calculated, what a good score is, what causes shifts and how to fix each cause, with code.
What does CLS measure?
web.dev defines CLS as the largest burst of layout shift scores for every unexpected layout shift that happens during the entire life of a page. A layout shift happens when a visible element changes its position from one rendered frame to the next.
Only existing elements that change their start position count. Adding a new element, or resizing one, is not a shift by itself. It becomes one when it pushes other visible elements out of place. So an image that loads and grows from zero height doesn't shift, but the paragraph it pushes down does.
Session windows
Shifts are grouped into bursts called session windows. A window holds shifts that happen less than 1 second apart, and lasts at most 5 seconds. Your CLS is the window with the highest total. CLS used to be the sum of every shift over the whole visit, and some tools can still report it that way.
How a single shift is scored
Each shift's score is the impact fraction multiplied by the distance fraction. The impact fraction is how much of the viewport the moving elements cover, counting both where they were and where they ended up. The distance fraction is the furthest any of them moved, divided by the viewport's largest dimension.
web.dev's worked example: an element filling half the viewport moves down by 25% of the viewport height. Before and after together it covers 75% of the viewport, so the impact fraction is 0.75. It moved 25% of the height, so the distance fraction is 0.25. The shift scores 0.75 × 0.25 = 0.1875. That single shift is enough to put the page into "needs improvement".
Which shifts are excluded
Shifts within 500 milliseconds of a tap, click or key press are flagged with hadRecentInput and left out, because the user expects the page to react. Scrolling, dragging and pinch-zooming do not count as input, so content that jumps while someone scrolls still counts against you.
What is a good CLS score?
The 75th percentile is what makes the score fair to real traffic. A page is rated good only if at least 75% of its page views have a CLS of 0.1 or less, and rated poor if at least 25% are above 0.25. Google measures mobile and desktop separately. A page passes the Core Web Vitals assessment when it meets the good threshold at the 75th percentile for all three metrics: LCP, INP and CLS.
Why 0.1? In Google's internal testing, shifts of 0.15 and higher were consistently seen as disruptive, while shifts of 0.1 and lower were noticeable but not excessively disruptive. CLS has no unit. It is not a time in seconds like the other two Core Web Vitals.
The common causes of layout shift
- Images and video without dimensions. The browser has no idea how tall they are until the file downloads, so it gives them no space and the text below jumps.
- Ads, embeds and iframes. web.dev calls ads one of the largest contributors to layout shifts on the web. Embedded videos, maps and social posts often don't know their own size until they load.
- Injected banners and forms. Cookie notices, promo bars and sign-up forms that pop in at the top of the page push everything else down. Content injected near the top usually causes bigger shifts than content lower down.
- Late web fonts. Text is laid out in a fallback font first. When the web font arrives and is a different size, the text block and everything around it moves, even if the text was invisible while it waited.
- Animating layout properties. Changing
toporleftcauses layout shifts, even when the element has its own layer. Animatingwidthandheightmoves the content around it.
How to fix each cause
Give every image and video its dimensions
Set width and height attributes, then let CSS make the image responsive: <img src="hero.jpg" width="1200" height="600" alt="..."> with img { width: 100%; height: auto; }. Browsers work out the aspect ratio from the two attributes before the image loads, so the space is reserved at the right height. Without attributes, use the CSS aspect-ratio property, for example aspect-ratio: 16 / 9.
With srcset, keep every candidate image at the same aspect ratio. With art direction in <picture>, put width and height on each <source> too.
Reserve space for ads and embeds
Give the slot a size before anything arrives, for example .ad-slot { min-height: 250px; }, or an aspect-ratio for responsive embeds. If ad sizes vary, reserve the most likely size or the smallest one and accept a small shift for larger ads. Never collapse the slot when no ad is returned. Show a placeholder instead, because removing reserved space shifts the page just as much as adding content does. Where you cannot reserve space, place the late content lower on the page.
Don't push content down without a tap
Show banners and notices in space you reserved in advance, or take them out of the document flow so they overlay the page, for example with position: fixed. For feeds and product lists, let people load new content with a "Load more" button. Shifts within 500 milliseconds of that tap don't count, and if the fetch takes longer, reserve the space straight away so the later arrival moves nothing.
Stop fonts from reflowing text
- Use
font-display: optional, so the web font is only used if it is ready by the first layout. - Always name a fallback, as in
font-family: "Brand Sans", sans-serif;. Without one, Chrome falls back to Times, a serif and a poor match. - Match the fallback to the web font with
size-adjust,ascent-override,descent-overrideandline-gap-override. - Load critical fonts early with
<link rel="preload">, so they have a better chance of arriving before the first paint.
Animate with transform
Use transform: translate() to move an element and transform: scale() to grow it, instead of changing top, left, width or height. Composited animations using translate cannot affect other elements, so they do not count towards CLS. Respect prefers-reduced-motion for visitors who are sensitive to movement.
Also: Make sure your pages are eligible for the back/forward cache. A page restored from it appears exactly as the visitor left it, with none of the shifts of a fresh load.
How to measure CLS in the lab and in the field
Lab tools load the page in a test environment: Chrome DevTools, Lighthouse, PageSpeed Insights and WebPageTest. Field tools report what real visitors experienced: the Chrome User Experience Report (CrUX), PageSpeed Insights, the Core Web Vitals report in Search Console and the web-vitals JavaScript library.
The two often disagree, and that is expected. Lab tools usually measure only the initial page load. CrUX measures the whole life of the page, including lazy-loaded content that shifts as people scroll and slow transitions in single-page apps. So lab CLS can be lower than what users see. PageSpeed Insights shows both: field data under "Discover what your real users are experiencing" and lab data under "Diagnose performance issues". A big gap between them usually means post-load shifts.
- Find the shifts. Record a trace in the DevTools Performance panel. The Layout Shifts track groups shifts into clusters, and clicking one shows what moved.
- Read Lighthouse carefully. It lists the elements that shifted, which are often the victims rather than the cause. Look at what was inserted or resized above them.
- Measure real users. Add
import { onCLS } from 'web-vitals'; onCLS(console.log);, or watch raw entries with aPerformanceObserverthat callsobserve({ type: 'layout-shift', buffered: true }).
Check: When PageSpeed Insights has no data for a URL, it may show data for your whole origin instead. Confirm which one you are looking at before hunting for a shift on the wrong page.
How ProCurveUX checks layout shift
ProCurveUX loads your page in a real browser at desktop and mobile sizes and records every layout shift that was not caused by input while the page loads. The "Content jumps while the page loads" check flags the page when the total passes 0.1, with the score and the number of shifts it saw. The "Images oversized, unsized or eagerly loaded" check flags images in view that have no dimensions, the most common cause. A static screenshot cannot see any of this, because a shift only exists while the page is moving. That is one reason we audit in a real browser.
Like any lab test, it sees one load of the page. It will catch shifts during loading, but not ones that happen later as someone scrolls. Confirm the result against field data in Search Console or PageSpeed Insights. You can see how findings are presented in a sample report.
Frequently asked questions
References
- web.dev (Google), Cumulative Layout Shift (CLS), definition, session windows, layout shift score; last updated April 12, 2023
- web.dev (Google), Optimize Cumulative Layout Shift, common causes and fixes; last updated February 7, 2025
- web.dev (Google), Web Vitals, 75th percentile assessment; last updated October 31, 2024
- web.dev (Google), Defining the Core Web Vitals metrics thresholds, good, needs improvement and poor thresholds; last updated May 7, 2025