Skip to content

SpeedPart of: Core Web Vitals

Cumulative Layout Shift (CLS): what it is and how to fix it

Cumulative Layout Shift scores how much a page jumps while you use it; good is 0.1 or less. Find the element that moves, then reserve its space with width and height, aspect-ratio or a fixed slot. Covers images, ads, embeds, banners and fonts.

getReport teamUpdated 27 Sept 202612 min read

Cumulative Layout Shift (CLS) is Google's score for how much visible content moves on its own while a page is used: the paragraph that jumps down as an image arrives, the "Buy" button that slides away as a banner appears. It has no unit; 0.1 or less is good and above 0.25 is poor, at the 75th percentile of real visits, and it is one of the three Core Web Vitals. The fix is almost always the same idea, reserve the space before the content arrives, and this guide shows how to find the element that moves and apply it to images, ads, embeds, banners and fonts. For how CLS fits with LCP and INP, see Core Web Vitals for site owners.

Quick answer

CLSRating
≤ 0.1Good
0.1-0.25Needs improvement
> 0.25Poor
  • Give every <img>, <video> and <iframe> width and height attributes. CSS can still scale it.
  • Reserve a fixed slot (min-height or aspect-ratio) for ads, embeds and anything a script fills in later.
  • Show cookie notices and promo bars as an overlay (position: fixed), never inserted above the content.
  • Load web fonts with font-display and a fallback font matched in size, so text does not reflow.
  • Animate with transform, not top, left, height or margin.
  • Run the speed test before and after; the target is 0.1 or less.

What is Cumulative Layout Shift?

A layout shift happens when an element that was already visible changes position between two frames without the visitor asking for it. CLS adds those shifts up and reports the worst stretch of them in the visit. It matters because shifts make people lose their place while reading and tap the wrong thing: the link that moved into the spot where "Add to cart" was a moment ago.

For Google, CLS is one of the Core Web Vitals its ranking systems use, judged on real Chrome visits. Unlike loading speed, it does not get better with faster hosting: a page on the fastest server in the world still jumps if an image has no dimensions. It is usually the cheapest vital to fix: most causes are one missing attribute or one line of CSS.

How CLS is scored

Each shift gets a score: the share of the viewport affected multiplied by how far things moved, as a share of the viewport. A banner that pushes the whole screen down by a quarter of its height scores about 0.25 on its own.

Shifts are grouped into bursts (shifts less than 1 s apart, a burst lasting at most 5 s), and CLS is the worst burst of the visit. The cumulative layout shift learn page has the one-paragraph version. Two details matter when you hunt for the cause:

  • Shifts within 500 ms of a tap, click or key press do not count. An accordion that opens when clicked is fine; one that opens by itself is not.
  • Shifts count for the whole visit, not just the load. A lab test only loads the page, so an ad that appears after 8 s or a sticky bar that appears on scroll shows up in the field data but not in the lab.

How getReport checks it

The speed test reports CLS from the Lighthouse lab run, which covers the page load on a simulated phone, and from the Chrome UX Report when Google has enough real visits. The finding shows the score and the fix:

The CLS finding card from the speed test: Cumulative Layout Shift above the 0.1 target, flagged as a warning, with the three fixes for images, fonts and injected content
The CLS finding shows the lab score against the 0.1 target and the fixes for the usual causes.

If the lab CLS is low but the field CLS is high, the shift happens after the load: a late ad, a banner on scroll, a font on a part of the page the lab did not reach. Look for those first.

Two more findings point at the most common causes directly. The HTML check reads every <img> and lists the ones that have no width or height attribute (SVGs are left out). The Lighthouse audit flags font files loaded without font-display:

The image size checker shows the same images in a table with their real pixel size and displayed size, which gives you the numbers for the attributes.

Step by step

1. Find the element that shifts

Before fixing anything, see what moves. In Chrome:

  1. Open DevTools, press Ctrl+Shift+P (Cmd+Shift+P on a Mac), type "Rendering" and open the Rendering tab.
  2. Tick Layout Shift Regions. Reload the page: every shifted area flashes blue.
  3. For detail, open the Performance panel, record a reload and look at the "Layout shifts" track. Clicking a shift shows which elements moved and by how much.

Throttle the network in DevTools (Network → Slow 4G) to see shifts the way a phone visitor does. Lighthouse 13 also lists the elements behind the shifts in its "Layout shift culprits" insight.

The element that moved is usually not the cause. Text that jumps down was pushed by something above it that grew: find the thing that arrived late.

2. Add width and height to images and videos

Browsers use the width and height attributes to compute the aspect ratio before the file arrives, and reserve that much space. The values are the image's real pixel size (or anything with the same proportions); your CSS still controls the displayed size:

HTML
<!-- The browser knows the box is 16:9 before the file loads -->
<img src="/img/team.webp" alt="Our team in the workshop" width="1600" height="900">
CSS
/* In your stylesheet: scale to the column, keep the ratio */
img, video {
  max-width: 100%;
  height: auto;
}

height: auto together with the attributes is what makes this work. A theme that sets height: 100% or a fixed pixel height on images breaks it; check the computed styles in DevTools if an image with dimensions still shifts.

For <video>, the same attributes apply; add a poster image so the reserved box is not empty.

3. Use aspect-ratio for boxes without intrinsic size

Iframes, background-image blocks, and containers that a script fills (a map, a YouTube embed, a product carousel) have no file dimensions. Give them a ratio in CSS:

CSS
/* YouTube and most video embeds are 16:9 */
.video-embed iframe {
  width: 100%;
  height: auto;
  aspect-ratio: 16 / 9;
}

/* A map block that a script fills in later */
.store-map {
  aspect-ratio: 4 / 3;
}

aspect-ratio is supported in every current browser.

4. Reserve slots for ads, embeds and injected content

Ad slots, newsletter sign-ups, review widgets and "related products" rows are filled by scripts after the page has drawn. Reserve their height in advance, sized for the most common content:

CSS
/* A 300×250 ad slot: the space exists before the ad script runs */
.ad-slot-sidebar {
  min-width: 300px;
  min-height: 250px;
}

/* A review widget that usually renders about 420 px tall */
.reviews-widget {
  min-height: 420px;
}

If the ad network sometimes returns nothing, keep the empty slot (with a subtle "Advertisement" label) rather than collapsing it; collapsing is a shift too. Place ad slots away from the top of the content where possible, since a shift near the top moves more of the page.

5. Stop inserting content above what is already there

Cookie notices, "free shipping" bars, app banners and "you are offline" notices that appear at the top of the page push everything down. Either render them in the HTML from the start (so they are there on the first paint), or show them as an overlay that sits on top of the content:

CSS
/* An overlay does not move anything else on the page */
.cookie-notice {
  position: fixed;
  inset: auto 0 0 0;   /* bottom of the viewport */
  z-index: 1000;
}

The same applies to content loaded by JavaScript: add new items below the visible area, or behind a button the visitor presses (shifts within 500 ms of the tap do not count).

6. Load web fonts without reflow

When a web font arrives, text that was drawn in a fallback font is redrawn. If the two fonts have different widths or line heights, lines rewrap and everything below moves. Two parts to the fix:

Tell the browser what to do while the font loads. font-display: swap shows fallback text at once and swaps when the font arrives; that fixes invisible text, which is what the font-display finding is about. font-display: optional goes further: if the font is not ready almost immediately, the fallback is kept for that page view, so there is no swap and no shift.

CSS
/* In the @font-face rule of a self-hosted font */
@font-face {
  font-family: "Brand Sans";
  src: url("/fonts/brand-sans.woff2") format("woff2");
  font-weight: 400;
  font-display: swap;
}

Make the fallback the same size. The size-adjust, ascent-override and descent-override descriptors stretch a local fallback font to match the web font's metrics, so a swap changes the letter shapes but not the line breaks:

CSS
/* A fallback tuned to Brand Sans; the values come from comparing both fonts */
@font-face {
  font-family: "Brand Sans Fallback";
  src: local("Arial");
  size-adjust: 104%;
  ascent-override: 92%;
  descent-override: 24%;
}

body {
  font-family: "Brand Sans", "Brand Sans Fallback", sans-serif;
}

The percentages depend on your fonts; tools that generate fallback metrics exist for most frameworks (Next.js does it automatically with next/font). Preloading the one or two fonts used above the fold also helps, because the swap happens before the first paint. The font-display learn page has the short version.

7. Animate with transform

Animating top, left, width, height or margin moves other elements on every frame, and each frame is a layout shift. transform and opacity move only the element itself and never count:

CSS
/* Shifts the layout on every frame */
.promo-bar { transition: margin-top 0.3s; }

/* Same visual effect, no layout shift */
.promo-bar { transition: transform 0.3s; }
.promo-bar.is-hidden { transform: translateY(-100%); }

Platform notes

WordPress

  • Images in posts get width and height automatically for images inserted through the editor (since WordPress 5.5). Images hard-coded in theme templates, page-builder widgets and old posts pasted as HTML often do not; those are the ones the images-missing-dimensions finding lists.
  • Lazy-load plugins that swap src for a 1×1 placeholder GIF and drop the dimensions reserve no space. Prefer WordPress core lazy loading, or check that the plugin keeps width and height; see Lazy loading images and iframes done right.
  • For builders, themes and plugins in more depth, see WordPress Core Web Vitals.
  • Cookie banner plugins that insert a bar at the top of the page are a classic CLS source. Choose the "bottom overlay" or "popup" layout in the plugin settings.
  • Sliders often render all slides stacked, then collapse them when the script runs. Set a fixed height or aspect-ratio on the slider container, or replace it with a single static image.

Shopify

Themes that use Shopify's image_tag Liquid filter output width and height automatically. App blocks (reviews, upsells, sticky add-to-cart bars) are the usual shifting elements; reserve min-height for them in the theme's CSS, or ask the app vendor whether it can render in a fixed-size slot.

Verify

  • Re-run the speed test: the CLS finding should be at or below 0.1, and the images-missing-dimensions finding should read "Every image declares width and height".
  • With Layout Shift Regions on, reload with Slow 4G throttling and scroll the whole page: nothing should flash blue except what you clicked.
  • Field CLS follows over the next 28 days; check it in the speed test or in Search Console under Core Web Vitals. If LCP or INP still fail, how to improve Core Web Vitals sets the order.

Common mistakes

  • Dimensions in the HTML, height: 100% in the CSS. Symptom: the image still shifts. Fix: use height: auto so the browser can apply the aspect ratio from the attributes.
  • Only testing the lab. Symptom: lab CLS 0.02, field CLS 0.3. Fix: the shift happens after load; scroll the page with Layout Shift Regions on, and check sticky bars and late widgets.
  • Hiding content with display: none until a script runs. Symptom: a block appears and pushes the page down a second after load. Fix: render it in place from the start, or reserve its height.
  • Fixing the element that moved instead of the one that caused it. Symptom: the footer "shifts" in DevTools, but the fix does nothing. Fix: look at what grew above it.

Questions people ask

What is Cumulative Layout Shift?

Cumulative Layout Shift is a Core Web Vital that measures how much visible content moves unexpectedly while a page is open, such as text jumping down when an image, ad or banner loads above it. It is a score without units: 0.1 or less at the 75th percentile of real visits is good, over 0.25 is poor. Reserving space for late content fixes most of it.

Why is my field CLS higher than the lab CLS?

Because the lab test only records the page load, while field CLS covers the whole visit on real devices. Ads that fill after several seconds, sticky bars that appear on scroll, consent banners, and fonts on parts of the page the lab never reached all add shifts that Lighthouse cannot see. Scroll the page with Layout Shift Regions switched on in DevTools to find them.

Do layout shifts after a click count towards CLS?

No, not if they happen within 500 ms of a click, tap or key press: the browser treats them as expected. Opening an accordion or a menu when the visitor asks for it is fine. Shifts that start later, or content that moves without any input, such as a banner sliding in or an auto-rotating slider changing height, do count.

Check your site before and after Check