Preventing Cumulative Layout Shift: A Practical Guide
Cumulative Layout Shift (CLS) measures the visual stability of a page — how much content moves unexpectedly while users are trying to read or interact with it. Of the three Core Web Vitals, CLS is often the easiest to fix and the most frustrating to experience. When a user reaches for a button and the page shifts, causing them to tap an ad instead, the trust in that experience is broken instantly.
A "Good" CLS score is 0.1 or less. Unlike LCP and INP, CLS is not measured in time — it is a unitless score calculated from the fraction of the viewport that shifts and the distance elements move. This guide covers every common cause of layout shift and provides concrete, copy-paste solutions for each.
How CLS Is Calculated
Each individual layout shift has a score computed as:
layout shift score = impact fraction × distance fraction
The impact fraction is the total area of the viewport affected by the shift — both the element's original position and its new position combined. The distance fraction is the greatest distance any element moved, divided by the viewport's largest dimension (usually height).
CLS uses the session window model: layout shifts are grouped into "session windows" of up to 5 seconds (with no more than 1 second between shifts). The page's final CLS is the largest session window score, not the sum of all shifts. This prevents long-lived single-page applications from accumulating unfairly high scores.
Important: Layout shifts caused by user interaction (clicking a button that expands a section, for example) are excluded from CLS. Only unexpected shifts — those not triggered within 500ms of a user input — count.
Images and Media Without Dimensions
The most common cause of CLS is images loaded without explicit width and height attributes. When the browser encounters an image tag without dimensions, it allocates zero space. When the image loads, the browser recalculates layout to accommodate it, pushing all subsequent content downward.
The Fix: Always Declare Dimensions
<!-- BAD: No dimensions — causes layout shift -->
<img src="/photo.webp" alt="Description">
<!-- GOOD: Explicit dimensions — browser reserves space -->
<img src="/photo.webp" alt="Description"
width="800" height="450">
<!-- ALSO GOOD: CSS aspect-ratio -->
<img src="/photo.webp" alt="Description"
style="aspect-ratio: 16/9; width: 100%; height: auto;">
The width and height attributes allow the browser to calculate the aspect ratio and reserve the correct vertical space before the image loads. Combined with CSS width: 100%; height: auto;, the image will be responsive while maintaining its aspect ratio — zero layout shift.
Responsive Images and srcset
When using srcset for responsive images, ensure all source images share the same aspect ratio. If different breakpoints serve different crops (e.g., a wide banner on desktop and a square crop on mobile), use the <picture> element with separate width/height on each <source>.
Video and iframe Embeds
Videos and iframes follow the same principle. Wrap them in a container with a fixed aspect ratio:
<div style="aspect-ratio: 16/9; width: 100%;">
<iframe src="https://www.youtube.com/embed/..."
style="width: 100%; height: 100%; border: 0;"
loading="lazy"
allow="accelerometer; autoplay; encrypted-media">
</iframe>
</div>
Web Font Loading and FOUT
When a web font loads and replaces the fallback font, differences in character width, line height, and x-height cause text to reflow — shifting surrounding content. This is called Flash of Unstyled Text (FOUT) and directly contributes to CLS.
Mitigation Strategies
font-display: optional: Uses the web font only if it is already cached from a prior visit. On the first visit, the fallback font is used permanently — no swap, no shift. This completely eliminates font-related CLS at the cost of not showing the custom font on first load.font-display: swap+ metric overrides: Thesize-adjust,ascent-override,descent-override, andline-gap-overrideCSS properties allow you to tune the fallback font's metrics to match the web font as closely as possible, minimizing reflow when the swap occurs.- Preload critical fonts:
<link rel="preload" as="font" crossorigin href="/fonts/main.woff2">starts the font download early, reducing the window during which the fallback font is visible.
/* Match fallback metrics to web font */
@font-face {
font-family: 'Adjusted Arial';
src: local('Arial');
size-adjust: 105%;
ascent-override: 95%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: 'Custom Font', 'Adjusted Arial', sans-serif;
}
Dynamically Injected Content
Content inserted into the DOM after initial render — banners, notifications, cookie consent bars, promotional headers — pushes existing content downward if inserted above it. This is one of the most impactful CLS contributors because it affects the entire page below the insertion point.
Strategies
- Reserve space: If a banner will appear, allocate its height in the layout from the start using a placeholder with
min-height. - Use overlays instead of insertions: Position dynamic content as a fixed or sticky overlay that does not displace page flow. Cookie banners fixed to the bottom of the viewport cause zero CLS.
- Insert below the fold: Dynamic content inserted below the current scroll position does not trigger CLS because the shifted elements are not visible in the viewport.
<!-- BAD: Injected banner pushes content down -->
<div id="promo-banner">Special offer!</div>
<main>Page content...</main>
<!-- GOOD: Fixed position overlay — no layout shift -->
<div id="promo-banner" style="position: fixed;
bottom: 0; left: 0; right: 0; z-index: 100;">
Special offer!
</div>
<main>Page content...</main>
<!-- GOOD: Reserved space with min-height -->
<div id="promo-slot" style="min-height: 60px;">
<!-- Banner loads here — space already reserved -->
</div>
<main>Page content...</main>
Advertising Slots
Ad slots are particularly problematic for CLS because their dimensions are often unknown until the ad network responds. A slot may receive a 300x250 ad, a 300x600 ad, or no ad at all — and the page must accommodate whichever arrives without shifting.
Best Practices for Ad Slots
- Set minimum dimensions: Give every ad container the minimum size of the most common ad format it serves (e.g.,
min-height: 250pxfor a medium rectangle slot). - Collapse gracefully: If no ad fills the slot, use a CSS transition to collapse the container smoothly rather than snapping to zero height. However, note that any collapse of a visible element counts as a layout shift unless triggered by user interaction.
- Place ads below the fold: Ads below the visible viewport do not cause CLS because the shifted content is not in view.
- Use sticky or fixed ad placements: Ads positioned with
position: stickydo not affect the flow of surrounding content.
CSS Animations That Cause Layout Shifts
Animations that modify layout-triggering CSS properties cause layout shifts on every frame. Properties like height, width, top, left, margin, and padding force the browser to recalculate layout for the entire subtree, and each recalculation can register as a layout shift.
Safe vs. Unsafe Animation Properties
| Safe (Compositor Only) | Unsafe (Trigger Layout) |
|---|---|
transform: translate() | top, left, right, bottom |
transform: scale() | width, height |
transform: rotate() | margin, padding |
opacity | border-width |
filter | font-size |
/* BAD: Animating height causes layout recalculation */
.m4-expand {
transition: height 0.3s ease;
height: 0;
}
.m4-expand.m4-active {
height: 200px;
}
/* GOOD: Use transform for visual movement */
.m4-slide-in {
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.m4-slide-in.m4-active {
transform: translateY(0);
}
Single-Page Application Navigations
SPAs that swap content during route transitions can produce layout shifts if the new content has different dimensions than the old content. This is especially common when navigating from a content-heavy page to a minimal one (or vice versa), causing the footer and sidebar to jump.
- Set
min-heighton the main content area: Prevents the page from collapsing during the transition between route contents. - Use skeleton screens: Show a placeholder layout matching the expected content structure before the actual content loads. This maintains visual stability during route transitions.
- Fade transitions: Crossfade between old and new content using
opacitytransitions, which do not trigger layout recalculation.
Debugging CLS Issues
Chrome DevTools provides several tools for identifying layout shift sources:
- Performance panel: Record a page load and look for "Layout Shift" entries in the timeline. Click on each shift to see which elements moved and by how much.
- Layout Shift Regions: Enable "Layout Shift Regions" in the rendering panel to see blue highlights flash on elements that cause layout shifts during page interaction.
- Web Vitals extension: The Chrome Web Vitals extension shows real-time CLS values and highlights the elements contributing to shifts.
For production monitoring, integrating CLS tracking with your APM system allows you to correlate layout shifts with specific user journeys, device types, and page templates. Synthetic monitoring with global checkpoints catches CLS regressions before they affect real users.
Key Takeaways
- CLS measures unexpected layout shifts — target ≤ 0.1 using the session window model
- Always set
widthandheight(oraspect-ratio) on images, videos, and iframes - Use
font-display: optionalor fallback metric overrides to eliminate font-swap shifts - Reserve space for dynamically injected content or use fixed/sticky positioning
- Set minimum dimensions on ad slots based on the most common ad format
- Animate only compositor properties (
transform,opacity) — never layout properties - Use skeleton screens during SPA route transitions to maintain visual stability
- Debug with DevTools Layout Shift Regions and monitor continuously with RUM