# How to fix Cumulative Layout Shift (CLS): images, fonts, ads, embeds

> Find the element that makes your page jump, then reserve its space with width and height, aspect-ratio or a min-height slot. Covers images, ads, embeds, banners and web fonts, with code.

Updated 2026-09-25 · Speed & Core Web Vitals · HTML version: https://getreport.app/guides/fix-cumulative-layout-shift

Layout shift is the page moving under the visitor: the paragraph jumps down as an image arrives, the "Buy" button slides away as a banner appears, a heading reflows when the web font loads. Cumulative Layout Shift (CLS) scores how much of that happens, and it is one of Google's 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.

## Quick answer

| CLS | Rating |
| --- | --- |
| ≤ 0.1 | Good |
| 0.1–0.25 | Needs improvement |
| > 0.25 | Poor |

- 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](https://getreport.app/tools/speed-test) before and after; the target is 0.1 or less.

## Why CLS matters

A shift is not just ugly. It makes people lose their place while reading, and it makes them tap the wrong thing: the link that moved into the spot where "Add to cart" was a moment ago. On mobile, where one shift can move the whole screen, it is the difference between a page that feels solid and one that feels broken.

For Google, CLS is one of the [Core Web Vitals](https://getreport.app/guides/core-web-vitals-for-site-owners) in the page experience signal, judged on real Chrome visits at the 75th percentile. 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. The good news is that it is usually the cheapest vital to fix. Most causes are one missing attribute or one line of CSS.

## How CLS is scored

Each time a visible element moves without the visitor doing anything, the browser scores the shift: 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](https://getreport.app/learn/cumulative-layout-shift) has the one-paragraph version. Two details matter when you hunt for the cause:

- **Shifts right after a tap, click or key press do not count** (within 500 ms). 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

> **Free tool:** [Website speed test](https://getreport.app/tools/speed-test): Lighthouse lab results and real-user Core Web Vitals for any page, mobile and desktop, with a filmstrip, a request waterfall and a fix for every slow part. Free, no signup.

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](https://getreport.app/guides/img/fix-cumulative-layout-shift/finding.webp "The CLS finding shows the lab score against the 0.1 target and the fixes for the usual causes.")

> **Check: Cumulative Layout Shift.** CLS measures how much the page jumps around while loading — the button that moves just as someone taps it. It is one of Google's three Core Web Vitals.
>
> 1. Give every <img>, <video>, ad slot and embed explicit width and height (or aspect-ratio) so space is reserved before it loads.
> 2. Preload the main web font and use font-display optional, or swap with a size-adjusted fallback font, so text does not reflow.
> 3. Never insert banners or content above existing content after load.

> **Check: Real-user Cumulative Layout Shift.** CLS on real visitors' devices over the last 28 days. Google's threshold for "good" is 0.1; layout jumps hit hardest on slow phones with late-loading ads and fonts.
>
> 1. Reserve space for images, embeds and ads; see the CLS finding above.

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`:

> **Check: Every image declares width and height.** Without width and height the browser cannot reserve space, so the text jumps when each image arrives. That is the most common cause of a poor Cumulative Layout Shift score.
>
> 1. Add width and height attributes with the image's real proportions; CSS can still scale it.
> 2. In WordPress, recent versions add them automatically for images inserted through the editor; hard-coded theme images need them by hand.

> **Check: Web fonts show text while loading.** Without font-display, browsers show blank text for up to 3 s while a web font downloads. Visitors stare at empty headings.
>
> 1. Add font-display swap (or optional) to every @font-face rule.
> 2. For Google Fonts, append &display=swap to the stylesheet URL; self-hosted fonts get it in the @font-face block.
> 3. Preload the one or two fonts used above the fold.

The [image size checker](https://getreport.app/tools/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; on a fast office connection many of them happen before the first paint and are invisible.

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="https://getreport.app/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](https://getreport.app/learn/font-display) 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 remove the dimensions reserve no space, then shift when the real image arrives. Prefer WordPress core lazy loading, or check that the plugin keeps `width` and `height`. See [Lazy loading images and iframes done right](https://getreport.app/guides/lazy-loading-done-right).
- **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.

## 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.
- **Collapsing an empty ad slot.** Symptom: CLS varies between visits. Fix: keep the reserved space even when no ad fills it.
- **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.
