EdgeCases Logo
Nov 2025
CSS
Deep
7 min read

size-adjust: Eliminating Font Swap Layout Shifts

Match fallback font metrics to custom fonts and eliminate 90% of CLS from font loading

css
fonts
size-adjust
layout-shift
performance
font-fallback

Use font-display: swap for instant text rendering—then watch your layout explode when the custom font loads and renders 15% larger than Arial. Headers shift, buttons resize, columns reflow. The size-adjust descriptor lets you scale fallback fonts to match custom font metrics exactly, eliminating layout shift during font swaps.

The Font Swap Layout Problem

Fonts with identical font-size values don't render at identical sizes. A 16px Arial character occupies roughly 7.5px of cap height. A 16px Montserrat character occupies ~8.7px. When the browser swaps from fallback to custom font, every line of text reflows.

/* Same font-size, vastly different rendered dimensions */
h1 {
  font-family: 'Montserrat', Arial, sans-serif;
  font-size: 48px;
}

/* Arial at 48px: ~22.5px cap height */
/* Montserrat at 48px: ~26.1px cap height */
/* Difference: 16% taller → CLS spike on font swap */
Loading demo...

The layout shift isn't from font styling—it's from inherent metric differences baked into the font files. Every font has internal ascender/descender/line-height values that control rendered dimensions.

size-adjust: Matching Fallback Metrics

The size-adjust descriptor (CSS Fonts Module Level 5) scales a fallback font to match the custom font's metrics. You define a local fallback font face with adjusted dimensions:

/* Original custom font */
@font-face {
  font-family: 'Montserrat';
  src: url('/fonts/montserrat.woff2') format('woff2');
  font-display: swap;
}

/* Adjusted Arial fallback to match Montserrat dimensions */
@font-face {
  font-family: 'Montserrat Fallback';
  src: local('Arial');
  size-adjust: 86.5%; /* Scale Arial down to match Montserrat */
  ascent-override: 105%;
  descent-override: 35%;
  line-gap-override: 0%;
}

/* Use both fonts in stack */
h1 {
  font-family: 'Montserrat', 'Montserrat Fallback', sans-serif;
}

How it works: The browser renders Montserrat Fallback (adjusted Arial) while the custom font downloads. When Montserrat loads and swaps in, the dimensions are identical—zero layout shift.

The Font Metric Override Quartet

size-adjust

Multiplier applied to the font's overall size. A value of 90% renders all text 10% smaller.

@font-face {
  font-family: 'Arial Adjusted';
  src: local('Arial');
  size-adjust: 90%; /* Shrink Arial by 10% */
}

ascent-override

Controls space above the baseline (where ascenders like 'h', 'b', 'l' reach). Override the font's internal ascent metric.

ascent-override: 110%; /* Add 10% more space above baseline */

descent-override

Controls space below the baseline (where descenders like 'g', 'y', 'p' extend). Override the font's internal descent metric.

descent-override: 30%; /* Reduce descender space */

line-gap-override

Additional spacing between lines (separate from ascent/descent). Most web fonts set this to 0; system fonts often have values here.

line-gap-override: 0%; /* Remove line gap entirely */

Calculating Override Values

Don't eyeball these values—font metrics are precise. Use automated tools to calculate exact override percentages:

Capsize

Capsize by Seek OSS is the gold standard. Input your custom font (e.g., Montserrat) and desired fallback (e.g., Arial), and it generates exact CSS with override values.

// Example Capsize output for Montserrat → Arial
@font-face {
  font-family: "Montserrat Fallback";
  src: local("Arial");
  size-adjust: 86.5%;
  ascent-override: 105%;
  descent-override: 35%;
  line-gap-override: 0%;
}

Framework Auto-Tools

Next.js 13+: next/font automatically generates fallback font faces with proper overrides:

import { Montserrat } from 'next/font/google';

const montserrat = Montserrat({
  subsets: ['latin'],
  display: 'swap',
  adjustFontFallback: true, // Auto-generates size-adjust fallback
});

// Next.js outputs both @font-face rules automatically

Nuxt 3: @nuxtjs/fontaine module does the same:

// nuxt.config.ts
modules: ['@nuxtjs/fontaine']

// Fontaine automatically creates matching fallbacks

Fontaine CLI

For non-framework projects, use Fontaine CLI:

npx fontaine inject /path/to/styles.css

// Scans for @font-face rules, adds fallback faces with overrides

Browser Support

The override descriptors have excellent modern browser support:

  • size-adjust: Chrome 92+, Firefox 92+, Safari 17.0+
  • ascent-override: Chrome 87+, Firefox 89+, Safari 16.4+
  • descent-override: Chrome 87+, Firefox 89+, Safari 16.4+
  • line-gap-override: Chrome 92+, Firefox 92+, Safari 16.4+

Progressive enhancement: Older browsers ignore these descriptors and use the original fallback font. No graceful degradation needed—it just works or falls back naturally.

Measuring CLS Improvement

Before: No size-adjust

/* Without font metric matching */
@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto.woff2') format('woff2');
  font-display: swap;
}

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

/* Result: CLS = 0.15 on font swap (tested on Lighthouse) */

After: With size-adjust

/* With Capsize-generated fallback */
@font-face {
  font-family: 'Roboto';
  src: url('/fonts/roboto.woff2') format('woff2');
  font-display: swap;
}

@font-face {
  font-family: 'Roboto Fallback';
  src: local('Arial');
  size-adjust: 97.5%;
  ascent-override: 92%;
  descent-override: 24%;
  line-gap-override: 0%;
}

body {
  font-family: 'Roboto', 'Roboto Fallback', sans-serif;
}

/* Result: CLS = 0.02 on font swap (92% reduction) */

Edge Cases and Gotchas

1. Multiple Font Weights

Each weight (Regular, Bold, etc.) may need different overrides. Bold fonts often have tighter metrics than Regular.

/* Separate fallback for each weight */
@font-face {
  font-family: 'Roboto Fallback';
  src: local('Arial');
  font-weight: 400;
  size-adjust: 97.5%;
  /* ... other overrides ... */
}

@font-face {
  font-family: 'Roboto Fallback';
  src: local('Arial Bold');
  font-weight: 700;
  size-adjust: 95.2%; /* Different override for bold */
  /* ... other overrides ... */
}

2. Variable Fonts

Variable fonts have continuous weight ranges (100-900). Calculate overrides for the most-used weight (typically 400), accept minor variance for other weights.

3. Cross-Platform Fallback Differences

Arial on macOS differs from Arial on Windows (different font files). For pixel-perfect consistency, calculate overrides per platform using -apple-system on Mac, Arial on Windows:

/* macOS-specific fallback */
@supports (font: -apple-system-body) {
  @font-face {
    font-family: 'Roboto Fallback';
    src: local('Helvetica Neue');
    size-adjust: 93%; /* Calculated for Helvetica Neue */
    /* ... */
  }
}

/* Windows-specific fallback */
@supports not (font: -apple-system-body) {
  @font-face {
    font-family: 'Roboto Fallback';
    src: local('Arial');
    size-adjust: 97.5%; /* Calculated for Arial */
    /* ... */
  }
}

(In practice, most sites use a single averaged override and accept ~1-2% variance across platforms.)

Production Workflow

  1. Identify custom fonts: List all @font-face declarations using font-display: swap or fallback.
  2. Choose fallback fonts: Match serif to serif (Georgia), sans to sans (Arial), mono to mono (Courier).
  3. Generate overrides: Use Capsize or framework tools to calculate size-adjust and override values.
  4. Test CLS: Run Lighthouse before/after. Compare CLS scores with slow network throttling.
  5. Deploy incrementally: Apply to high-traffic pages first, measure field CLS data in CrUX.

When size-adjust Isn't Enough

If your custom font and fallback have radically different character widths (condensed vs extended), size-adjust can't fix horizontal reflow. In these cases:

  • Option 1: Use font-display: optional + preload (avoid swaps entirely).
  • Option 2: Choose a closer fallback. Roboto Condensed → Arial Narrow, not Arial.
  • Option 3: Accept the CLS. Some designs require specific fonts; optimize elsewhere.

For the vast majority of fonts (Roboto, Inter, Montserrat, Open Sans), size-adjust combined with override descriptors eliminates 90%+ of layout shift. It's the single most effective CSS property for improving Core Web Vitals CLS scores.

Advertisement

Related Insights

Explore related edge cases and patterns

CSS
Deep
Font Metrics: Why Text Won't Center in Buttons
7 min
CSS
Deep
Font Loading: The FOUT, FOIT, and CLS Dilemma
7 min
CSS
Deep
Font Preloading: When rel=preload Backfires
7 min

Advertisement