# Cumulative layout shift (CLS)

> Content that jumps as the page loads fails Core Web Vitals and mis-taps users. Reserve space for images, ads and late content to stop the shift.

_Category: Performance · Detector `layout-thrashing` · Severity: warning_

Layout shift is when content jumps after it has already rendered — an image loads and pushes the text down, a banner injects itself above the fold, a web font swaps and reflows everything. It is a Core Web Vitals metric Google ranks on, and it makes users tap the wrong thing. AI-built pages shift constantly because generated markup omits `width`/`height` on images and drops late content in without reserving space.

## Symptoms

- Text and buttons jump down as images or ads load in
- You tap one thing and hit another because it moved
- A visible reflow when the web font swaps in
- PageSpeed Insights reports a poor CLS score (> 0.1)

## How VibeCheck detects it

The `layout-thrashing` detector flags this live in the browser and reports it to the widget's Problems list — and to your coding agent over MCP.

- **Issue string:** `Layout shift cluster detected (3 shifts)`
- **Threshold:** ≥ 3 layout shifts within a 500ms window, with no recent user input

## Root causes

- Images and videos without `width`/`height` (or an `aspect-ratio` box) reserving space
- Content injected above existing content (banners, cookie bars, ads) after load
- Web fonts swapping and reflowing text (FOUT) without a matched fallback
- Dynamically sized containers that resize once data arrives

## The fix

Reserve the final space before the content arrives. Give every image and embed explicit dimensions or a CSS `aspect-ratio` box, insert late content into a pre-sized container (never above existing content), and size fallback fonts to match the web font so the swap doesn’t reflow.

### Steps

1. Add `width` and `height` (or `aspect-ratio`) to every image, video, and iframe
2. Reserve a fixed-size slot for anything injected after load (ads, banners, embeds)
3. Use `font-display: optional` or a size-adjusted fallback to avoid font-swap reflow

_Reserve space with aspect-ratio_

```css
.media {
  aspect-ratio: 16 / 9;
  width: 100%;
}
/* size the fallback to the web font so the swap doesn't reflow */
@font-face {
  font-family: 'Inter Fallback';
  src: local('Arial');
  size-adjust: 107%;
}
```

## Framework-specific fixes

### React

Always pass `width` and `height` to `<img>` so the browser can reserve the box before the file loads, even when CSS scales it responsively.

```tsx
<img
  src="/hero.jpg"
  width={1200}
  height={630}
  alt="Hero"
  style={{ width: '100%', height: 'auto' }}
/>
```

### Next.js

`next/image` reserves space from the `width`/`height` (or fill + a sized parent) automatically, and `next/font` eliminates font-swap shift by self-hosting with a size-adjusted fallback. Using both removes the two most common CLS sources.

```tsx
import Image from 'next/image'
import { Inter } from 'next/font/google'

const inter = Inter({ subsets: ['latin'] }) // no layout shift on swap

<Image src="/hero.jpg" width={1200} height={630} alt="Hero" />
```

[Next.js docs](https://nextjs.org/docs/app/api-reference/components/image)

### Vanilla JS

Set the `width` and `height` attributes on media, and wrap embeds in an `aspect-ratio` box. Never `document.body.prepend()` content above what the user is already reading.

```html
<img src="/hero.jpg" width="1200" height="630" alt="Hero" style="width:100%;height:auto" />
```

## FAQ

### What CLS score do I need?

Google considers CLS good at 0.1 or below, and poor above 0.25, for 75% of page loads. It is one of the three Core Web Vitals used in ranking.

### Why does the layout-thrashing detector flag this?

VibeCheck observes the browser’s layout-shift entries. When three or more fire within 500ms without user input, it flags a cluster — that burst is exactly what a jumpy load feels like.

### My image is responsive — do I still set `width` and `height`?

Yes. Set the intrinsic `width`/`height` attributes so the browser knows the aspect ratio, then use CSS (`width:100%`; `height:auto`) to scale it. That gives you responsiveness with zero shift.

## Related problems

- [Unoptimized images](https://vibecheck.wcgw.fun/fix/unoptimized-images.md) — Performance
- [Large image files](https://vibecheck.wcgw.fun/fix/large-image-files.md) — Performance
- [Excessive DOM size](https://vibecheck.wcgw.fun/fix/excessive-dom-size.md) — Performance
- [Long tasks blocking the main thread](https://vibecheck.wcgw.fun/fix/long-tasks.md) — Performance

---

Fix guide from VibeCheck — https://vibecheck.wcgw.fun/fix/cumulative-layout-shift. Full site index for LLMs: https://vibecheck.wcgw.fun/llms.txt
