Guideadvanced

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.

Rankwise Team·Updated Apr 13, 2026·5 min read

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);
}

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)
  • 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

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

  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:

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

Part of the SEO Fundamentals topic

Newsletter

Stay ahead of AI search

Weekly insights on GEO and content optimization.