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

**Audit ID:** `cumulative-layout-shift` · **Category:** Performance · **Core Web Vital**

CLS measures unexpected layout movement during page load. It's one of Google's three Core Web Vitals, a stable, predictable page ranks better and converts better.

## TL;DR

- **What:** Sum of all unexpected layout shifts during the page's lifecycle.
- **Target:** Under 0.1 (the "Good" threshold).
- **Top three fixes:** set explicit `width`/`height` on images, reserve space for ads/embeds, avoid inserting content above existing content.

## What CLS measures

Every time a visible element jumps to a new position without user input, CLS increases. The metric is `impact fraction × distance fraction`, summed over the page session. A button that moves down 100px right before the user clicks counts heavily; a tiny shift in a far corner barely registers.

## What's a good CLS score?

| Range | Verdict |
|---|---|
| ≤ 0.1 | Good |
| 0.1–0.25 | Needs improvement |
| > 0.25 | Poor |

## Most common causes

1. **Images without dimensions**, the browser can't reserve space until it downloads them
2. **Web fonts** swapping (FOUT/FOIT) and reflowing text
3. **Ads, embeds, iframes** without reserved containers
4. **Dynamically injected content** above existing content (banners, cookie notices, A/B test variants)
5. **Animations using `top`/`left`** instead of `transform`

## Five concrete fixes

### 1. Always set `width` and `height` on images

```html
<!-- WRONG: no dimensions, layout shifts when image loads -->
<img src="/photo.jpg" alt="…" />

<!-- RIGHT: dimensions reserve the aspect ratio -->
<img src="/photo.jpg" width="1200" height="800" alt="…" />
```

Modern browsers compute `aspect-ratio` automatically from these attributes, the slot is reserved before download finishes.

### 2. Reserve space for ads, embeds, iframes

```css
.ad-slot {
  min-height: 250px; /* reserve whatever the typical ad height is */
  width: 100%;
}
```

If you don't know the ad height in advance, use a placeholder with a typical aspect ratio.

### 3. Avoid inserting content above existing content

Don't push existing content down. If you must inject a cookie banner or promo bar at the top of the viewport, either:
- Reserve its space ahead of time with a CSS `min-height` on `<body>`
- Position it `fixed` over the content rather than displacing it

### 4. Stabilize web fonts

```css
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter.woff2') format('woff2');
  font-display: optional; /* or swap with size-adjust */
  size-adjust: 100%;
}
```

Or preload the font so it lands before FCP:

```html
<link rel="preload" as="font" type="font/woff2" href="/fonts/inter.woff2" crossorigin />
```

### 5. Animate `transform`, not layout properties

```css
/* WRONG: triggers layout */
.menu { left: 0; transition: left 0.3s; }

/* RIGHT: uses GPU compositing, no layout */
.menu { transform: translateX(0); transition: transform 0.3s; }
```

## Verification

1. Re-run Lighthouse, CLS should be < 0.1.
2. Test on slow 3G in DevTools to catch font/image shifts that don't appear on fast networks.
3. Check Chrome DevTools → Performance → Experience panel for individual shift events.

## Related audits

- [Largest Contentful Paint (LCP)](/audits/largest-contentful-paint), load speed
- [Total Blocking Time (TBT)](/audits/total-blocking-time), main-thread blocking
- [Image alt attributes](/audits/image-alt), accessibility on images

---

Audit your URL at https://lighthouse-md.com.
