Most CLS optimization advice focuses on fixing existing shifts. This guide takes the opposite approach: how to architect web pages so layout shifts never happen in the first place. Prevention is cheaper than remediation — building stability into your component library, build pipeline, and content templates eliminates entire categories of CLS issues permanently.
Principle 1: Always reserve space
Every element that loads asynchronously needs its space reserved before it arrives. This is the single most important architectural decision for CLS prevention.
Images: aspect-ratio as a default
Build your image component to always output dimensions:
<!-- Image component always includes dimensions -->
<img
src="photo.jpg"
alt="Description"
width="1200"
height="630"
style="aspect-ratio: 1200/630; width: 100%; height: auto;"
loading="lazy"
decoding="async"
/>
If you use a CMS or markdown pipeline, ensure the image processing step extracts and embeds dimensions automatically. Never rely on content authors to add dimensions manually.
Embeds: container-first pattern
For third-party embeds (YouTube, Twitter, maps), wrap them in a sized container:
.embed-container {
aspect-ratio: 16 / 9;
width: 100%;
background: var(--surface-secondary);
border-radius: 8px;
overflow: hidden;
}
.embed-container iframe {
width: 100%;
height: 100%;
border: 0;
}
The container reserves space immediately. The iframe fills it when it loads. Zero shift.
Ad slots: min-height reservation
.ad-slot[data-size="leaderboard"] {
min-height: 90px;
}
.ad-slot[data-size="rectangle"] {
min-height: 250px;
}
.ad-slot[data-size="skyscraper"] {
min-height: 600px;
}
Set minimum heights based on your ad unit sizes. If the ad doesn't fill, the reserved space persists — better than a shift.
Principle 2: Fonts that don't reflow
Font loading is a site-wide CLS source. Fix it once in your CSS architecture and every page benefits.
The zero-shift font stack
/* Define fallback with matching metrics */
@font-face {
font-family: "Brand Font";
src: url("/fonts/brand.woff2") format("woff2");
font-display: swap;
size-adjust: 104.7%;
ascent-override: 92%;
descent-override: 22%;
line-gap-override: 0%;
}
/* System fallback matches Brand Font metrics */
body {
font-family:
"Brand Font",
-apple-system,
BlinkMacSystemFont,
sans-serif;
}
Tools like Fontaine or Capsize calculate the exact size-adjust and override values for your font/fallback combination. The result: when the custom font loads, the text doesn't move because the metrics match.
Preload critical fonts
<link
rel="preload"
href="/fonts/brand.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link
rel="preload"
href="/fonts/brand-bold.woff2"
as="font"
type="font/woff2"
crossorigin
/>
Preloading reduces the time between fallback rendering and font swap, further minimizing any residual shift.
Subset fonts aggressively
Smaller font files load faster. If your site only uses Latin characters, subset the font to exclude Cyrillic, Greek, and CJK glyphs. Use pyftsubset or Google Fonts' &text= parameter to reduce file size by 60-80%.
Principle 3: CSS containment by default
CSS containment tells the browser that an element's layout is independent. Changes inside a contained element don't trigger layout recalculation for the rest of the page.
Component-level containment
/* Every card, section, and widget gets containment */
.card,
.section,
.widget {
contain: layout style;
}
This prevents a single element's size change from cascading into a full-page reflow.
Content-visibility for long pages
/* Below-fold sections skip rendering until scrolled into view */
.below-fold {
content-visibility: auto;
contain-intrinsic-size: 0 400px;
}
content-visibility: auto dramatically improves initial render performance. The contain-intrinsic-size value should estimate the section's rendered height to prevent scrollbar jumps.
Principle 4: No inline content injection above the fold
Dynamic content injected above the fold is the most common source of user-visible shifts.
Fixed-position overlays instead of inline banners
/* Bad: pushes content down */
.banner-inline {
padding: 16px;
background: var(--warning);
}
/* Good: overlays without shifting */
.banner-fixed {
position: fixed;
top: 0;
left: 0;
right: 0;
padding: 16px;
background: var(--warning);
z-index: 1000;
transform: translateY(-100%);
transition: transform 0.3s ease;
}
.banner-fixed.visible {
transform: translateY(0);
}
Cookie banners, notifications, and alerts
Always use position: fixed or position: sticky for elements that appear after initial render. Never insert them as normal flow elements at the top of the page.
Late-loading hero sections
If your hero section depends on an API call or async data:
.hero {
min-height: 500px; /* Reserve space for the hero */
display: grid;
place-items: center;
}
Reserve the space even before the content loads. Show a skeleton or background color in the reserved area.
Principle 5: Animations that don't shift
Safe animation properties
These properties run on the compositor thread and never cause layout shifts:
transform(translate, scale, rotate)opacityfilterclip-path
Unsafe animation properties
These properties trigger layout recalculation and create shifts:
width,heighttop,right,bottom,leftmargin,paddingborder-widthfont-size
Expand/collapse pattern
/* Bad: animating height causes layout shift */
.accordion-panel {
height: 0;
overflow: hidden;
transition: height 0.3s;
}
/* Good: use grid for intrinsic size animation */
.accordion-panel {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 0.3s;
overflow: hidden;
}
.accordion-panel.open {
grid-template-rows: 1fr;
}
.accordion-panel > div {
min-height: 0;
}
The grid-template-rows: 0fr → 1fr technique animates height without triggering layout shifts because it doesn't change the element's layout participation during the transition.
Principle 6: Build pipeline enforcement
Lighthouse CI with CLS budget
# lighthouserc.js
module.exports = {
ci: {
assert: {
assertions: {
'cumulative-layout-shift': ['error', { maxNumericValue: 0.1 }],
},
},
},
};
Fail the build if any page exceeds the CLS threshold. This catches regressions before they reach production.
Image dimension validation
Add a build step that validates every image in your content pipeline has explicit dimensions:
# Check all img tags have width and height
grep -rn '<img' dist/ | grep -v 'width=' | grep -v 'svg'
If any images lack dimensions, fail the build.
CSS audit for layout-triggering animations
Lint your CSS for animation properties that cause shifts:
# Flag animations that use layout-triggering properties
grep -rn 'transition:.*\(width\|height\|top\|left\|margin\|padding\)' src/
Architecture checklist
Use this checklist when designing new page templates or components:
- Every
<img>has width/height or aspect-ratio - Every embed is in a sized container
- Ad slots have min-height reservations
- Fonts use metric overrides for zero-reflow swaps
- Critical fonts are preloaded
- Dynamic banners use fixed/sticky positioning
- Animations use transform/opacity only
- Components use
contain: layout style - Below-fold sections use content-visibility
- Lighthouse CI enforces CLS < 0.1
Frequently Asked Questions
Is it possible to achieve CLS of exactly zero?
On simple static pages, yes. Pages with any dynamic content (ads, user-generated content, API-driven sections) will typically have a very small CLS (0.001-0.01) even with perfect prevention. The goal is under 0.05, not exactly zero.
Should I use content-visibility everywhere?
Only on sections below the initial viewport. Applying content-visibility: auto to above-fold content delays its rendering, hurting LCP. Use it for long pages with sections the user has to scroll to reach.
How do I handle responsive images that change aspect ratio at breakpoints?
Use the <picture> element with different sources per breakpoint, each with explicit width/height:
<picture>
<source
media="(min-width: 768px)"
srcset="wide.jpg"
width="1200"
height="400"
/>
<img src="mobile.jpg" alt="..." width="400" height="400" />
</picture>
Does CSS Grid or Flexbox affect CLS?
Neither inherently causes CLS. However, layouts where children determine parent size (flex-grow, auto-height grids) can shift when late-loading children appear. Use explicit min-height or aspect-ratio on containers that wrap async-loading content.
Related Resources
- Eliminating Layout Shifts — Fix existing CLS issues
- Understanding CLS — What CLS measures and how it's calculated
- Cumulative Layout Shift — CLS glossary definition
- Performance Audit Template — Run a structured CWV audit