# Preventing Layout Shifts: Architectural Patterns for Stable Web Pages

> Architectural patterns and CSS strategies that prevent Cumulative Layout Shift at the design level. Build pages that never shift instead of fixing shifts after launch.

Canonical page: [https://tryrankwise.com/resources/guides/preventing-layout-shifts](https://tryrankwise.com/resources/guides/preventing-layout-shifts). Updated 2026-04-13. Published by Rankwise (tryrankwise.com).

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:

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

```css
.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

```css
.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, which is 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

```css
/* 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

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

```css
/* 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

```css
/* 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

```css
/* 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:

```css
.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)
- `opacity`
- `filter`
- `clip-path`

### Unsafe animation properties

These properties trigger layout recalculation and create shifts:

- `width`, `height`
- `top`, `right`, `bottom`, `left`
- `margin`, `padding`
- `border-width`
- `font-size`

### Expand/collapse pattern

```css
/* 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

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

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

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

1. Every `<img>` has width/height or aspect-ratio
2. Every embed is in a sized container
3. Ad slots have min-height reservations
4. Fonts use metric overrides for zero-reflow swaps
5. Critical fonts are preloaded
6. Dynamic banners use fixed/sticky positioning
7. Animations use transform/opacity only
8. Components use `contain: layout style`
9. Below-fold sections use content-visibility
10. 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:

```html
<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](/resources/guides/eliminating-layout-shifts): Fix existing CLS issues
- [Understanding CLS](/resources/learn/understanding-cls): What CLS measures and how it's calculated
- [Cumulative Layout Shift](/glossary/cumulative-layout-shift): CLS glossary definition
- [Performance Audit Template](/templates/performance-audit): Run a structured CWV audit
