loke.dev
Header image for 4 `@font-face` Descriptors That Will Finally Rescue Your Cumulative Layout Shift

4 `@font-face` Descriptors That Will Finally Rescue Your Cumulative Layout Shift

Stop the jarring jumps when web fonts load by forcing your fallback typography to occupy the exact same footprint as your brand font.

· 4 min read

4 @font-face Descriptors That Will Finally Rescue Your Cumulative Layout Shift

You’ve spent hours perfecting your typography, but your users are spending those first few seconds dodging a shifting layout that feels like a glitchy PowerPoint presentation. It’s a classic tragedy: your brand font finally downloads, but because it’s 10% wider than the fallback font, the entire paragraph jumps down three lines, pushing your "Buy Now" button into the abyss.

This is the Cumulative Layout Shift (CLS) nightmare. Most developers try to fix this with font-display: swap, which is great for getting text on the screen quickly, but it actually *creates* the layout shift when the swap occurs.

To fix this for real, we need to stop treating our fallback fonts like ugly stepchildren and start forcing them to behave exactly like our primary brand fonts. Here are the four CSS descriptors that let you "match-fix" your typography metrics.

The Problem: The "Ghost" Footprint

When a browser loads a page, it looks at your stack: font-family: "BrandFont", Arial, sans-serif;. While "BrandFont" is downloading, it uses Arial. But Arial has its own x-height, cap-height, and character width. If "BrandFont" is narrower than Arial, everything snaps back when it finishes loading.

We used to solve this with weird JavaScript hacks or by just accepting that the web is a janky place. We don't have to do that anymore.

1. size-adjust

This is the heavy hitter. size-adjust allows you to scale the glyphs of a font without changing the font-size property in your CSS.

If your brand font is naturally "smaller" than Arial at 16px, you can create a local version of Arial and scale it down to match the footprint of your custom font.

/* Create a 'fake' version of Arial that mimics our brand font's scale */
@font-face {
  font-family: 'Arial-Adjusted';
  src: local('Arial');
  size-adjust: 92%; /* Shrinks Arial to match the brand font's width/height */
}

body {
  font-family: 'BrandFont', 'Arial-Adjusted', sans-serif;
}

By scaling the fallback, the "switch" from Arial-Adjusted to BrandFont becomes almost invisible because they occupy the same horizontal space.

2. ascent-override

Ever notice how some fonts seem to have weirdly large gaps above the letters? That’s the ascent. If your fallback font has a higher ascent than your brand font, your line-height will look massive until the custom font kicks in.

ascent-override lets you manually set the height of the space above the baseline.

@font-face {
  font-family: 'Fallback-Matching';
  src: local('Times New Roman');
  ascent-override: 85%; /* Pulls the 'top' of the font down */
}

3. descent-override

The sibling to ascent. This controls the space *below* the baseline. If your custom font has long descenders (the tails on 'g', 'j', and 'p') but your fallback doesn't, the page will "jump" vertically once the custom font renders.

I’ve found that tweaking these two in tandem is usually enough to stop 90% of vertical layout shifts in long-form blog posts.

@font-face {
  font-family: 'Fallback-Matching';
  src: local('Arial');
  descent-override: 20%;
}

4. line-gap-override

This corresponds to the "leading" in traditional typography. Even if your glyphs match perfectly, different fonts have different default gaps between lines. This descriptor lets you override that internal spacing.

If you’re using a font like Inter or Roboto, they usually have very tight line-gaps. Standard system fonts like Georgia are much more "breathable." Using line-gap-override: 0% on a fallback can help tighten things up to prevent that extra vertical bloat.

Putting it all together: The "Perfect" Fallback

You don't just use one of these; you use them together to create a "shadow" font that is a structural clone of your brand font.

Here is what a production-ready implementation looks like. Let's say we're using a custom font called "HeavyBrand."

/* 1. Define our custom font as usual */
@font-face {
  font-family: 'HeavyBrand';
  src: url('/fonts/heavy-brand.woff2') format('woff2');
  font-display: swap;
}

/* 2. Define a "Metric-Compatible" fallback */
@font-face {
  font-family: 'HeavyBrand-Fallback';
  src: local('Arial');
  size-adjust: 94%;
  ascent-override: 90%;
  descent-override: 18%;
  line-gap-override: 0%;
}

/* 3. Apply the stack */
h1, p {
  font-family: 'HeavyBrand', 'HeavyBrand-Fallback', sans-serif;
}

How do you find these numbers?

You could spend three hours refreshing your browser and squinting at your screen, but please don't do that.

The easiest way is to use a tool like the Capsize web app or Fontaine. These tools allow you to drop in your web font, select a fallback (like Arial or Roboto), and they will literally spit out the exact percentages you need to make them match.

One "Gotcha" to watch for

These descriptors only work inside an @font-face block. You can't just slap size-adjust: 90% on a p tag and expect it to work. You are essentially defining a new version of a local font with modified metadata.

It feels like a lot of extra CSS at first, but compared to the SEO penalty of a high CLS score, it’s a bargain. Stop letting your fonts push your users around. Give your fallback the respect (and the math) it deserves.