size-adjust: Eliminating Font Swap Layout Shifts
Match fallback font metrics to custom fonts and eliminate 90% of CLS from font loading
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 */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 automaticallyNuxt 3: @nuxtjs/fontaine module does the same:
// nuxt.config.ts
modules: ['@nuxtjs/fontaine']
// Fontaine automatically creates matching fallbacksFontaine CLI
For non-framework projects, use Fontaine CLI:
npx fontaine inject /path/to/styles.css
// Scans for @font-face rules, adds fallback faces with overridesBrowser 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
- Identify custom fonts: List all
@font-facedeclarations usingfont-display: swaporfallback. - Choose fallback fonts: Match serif to serif (Georgia), sans to sans (Arial), mono to mono (Courier).
- Generate overrides: Use Capsize or framework tools to calculate
size-adjustand override values. - Test CLS: Run Lighthouse before/after. Compare CLS scores with slow network throttling.
- 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
Explore these curated resources to deepen your understanding
Official Documentation
Tools & Utilities
Related Insights
Explore related edge cases and patterns
Advertisement