← All Blogs

Fixing Layout Shift (CLS): Images, Fonts, and Stable UI in Real Apps

Fixing Layout Shift (CLS) cover

by Abdelkader Settah

March 04, 2026

Fixing Layout Shift (CLS): Images, Fonts, and Stable UI in Real Apps

Layout shift is the moment a page moves under the user’s cursor: headlines jump, buttons slide, and the UI feels “cheap” even if it looks beautiful.

In Core Web Vitals, this is measured as CLS (Cumulative Layout Shift). The good news: most CLS issues come from a small set of causes you can fix systematically.

This post walks through a real-world approach to:

  • find what’s shifting,
  • fix it at the root,
  • and verify the CLS improvement.

Before you start: reproduce and measure (don’t guess)

Use at least one of:

  • Chrome DevTools → Performance panel (look for “Layout Shift” events)
  • Lighthouse (in Chrome) for a baseline score
  • Web-vitals instrumentation (if you have it in-app)

The point: identify which element is moving, and why.

The top 3 CLS causes (and fixes)

1) Images without reserved space

If the browser doesn’t know an image’s dimensions, it can’t reserve space. When the image loads, it pushes content down.

Fix:

<!-- Good: reserve space -->
<img
  src="/hero.webp"
  alt="Product hero"
  width="1200"
  height="630"
  loading="eager"
  decoding="async"
/>

If you don’t have fixed dimensions (e.g., CMS images), use CSS aspect-ratio:

.cardMedia {
  aspect-ratio: 16 / 9;
  width: 100%;
  object-fit: cover;
}

2) Fonts causing text to “reflow”

Web fonts can cause shifts when:

  • the fallback font has different metrics than the final font,
  • or the browser hides text until the font loads (FOIT).

Fix baseline:

/* If you self-host fonts */
@font-face {
  font-family: "MyFont";
  src: url("/fonts/MyFont.woff2") format("woff2");
  font-display: swap;
}

Fix better (advanced): use metric-compatible fallbacks via size-adjust, ascent-override, descent-override, line-gap-override. This reduces visible jumps when the real font loads.

If you use Google Fonts, also ensure you’re preloading in a non-blocking way (your site already does this in src/components/MainHead.astro).

3) Dynamic content inserted above the fold

Common culprits:

  • cookie banners / consent bars
  • “announcement” bars
  • async personalization / A/B content
  • error banners that appear after fetch

Fix: reserve space or overlay instead of pushing content:

.topBanner {
  position: sticky;
  top: 0;
  min-height: 48px; /* reserve space */
}

Or, for temporary banners, consider position: fixed with a safe body offset that’s applied immediately (not after hydration).

Practical debugging: “what moved?”

When you spot a shift:

  • Inspect the element’s box model before/after.
  • Look for late-applied CSS classes (e.g., async theme, font class, hydration toggles).
  • Check if placeholders/skeletons have the same dimensions as the final content.

Verify the improvement

After fixes:

  • rerun Lighthouse (same route, similar throttling),
  • re-check DevTools Performance for fewer/smaller “Layout Shift” events,
  • validate on slower devices or throttled CPU (shifts are easier to reproduce).

Closing

If you treat CLS like a checklist—images, fonts, dynamic insertions—you’ll fix most layout shift problems quickly. The key is to measure, fix at the source, and verify.