Layout-Verschiebung (CLS) durch Font-Loading verhindern
Preventing Layout Shift (CLS) from Font Loading
Cumulative Layout Shift measures visual instability — the degree to which page elements move unexpectedly after initial render. A score below 0.1 is considered "Good" by Google's Core Web Vitals standards. A score above 0.25 is "Poor" and affects search ranking.
Fonts are one of the most consistent sources of poor CLS scores. When a browser renders text with a fallback system font and then swaps in a custom typeface with different metrics, paragraphs reflow. Text blocks shrink or expand. Elements below the text jump. On pages with large text blocks, a single font swap can push CLS from 0.02 to 0.3 — entirely in the "Poor" range.
This guide explains why font swapping causes layout shift and walks through every technique for eliminating or minimizing it.
How Fonts Cause Layout Shift
Every typeface has a set of typographic metrics that define how it occupies space:
- Ascent: The distance from the baseline to the top of the tallest character
- Descent: The distance from the baseline to the bottom of descending characters (g, y, p)
- Line gap: Additional spacing added between lines of text
- Advance width: The horizontal space each character occupies
- x-height: The height of lowercase letters relative to capitals
When a fallback font (Arial, Helvetica, Times New Roman) has different metrics than the custom font (Inter, Roboto, Lato), text occupies different amounts of vertical and horizontal space. A paragraph that takes 5 lines in Arial might take 4.5 lines in Inter — or 5.5 lines. When the custom font swaps in, the paragraph expands or contracts, pushing every element below it up or down.
The amount of shift depends on how different the two fonts' metrics are. Some fonts have very similar metrics — Inter and Helvetica are reasonably close. Others are dramatically different — a condensed display font might be 30% narrower than the fallback, causing entire text blocks to reflow extensively.
The timing also matters. A font that loads before the initial paint causes no CLS (there's never a fallback state). A font that loads 200ms after first paint causes modest CLS because little user-initiated scrolling has happened. A font that loads 3 seconds after first paint — during active reading — causes CLS that users directly experience as content jumping beneath their eyes.
Measuring Font-Related CLS
Before fixing CLS, you need to confirm that fonts are the source and quantify the magnitude.
Chrome DevTools Layout Shift Regions
In Chrome DevTools, enable the "Layout Shift Regions" overlay: Open DevTools → More tools → Rendering → Layout Shift Regions. This highlights every element that shifts in blue when it moves. Load your page and watch for font-swap events — the blue overlays will appear precisely when the custom font loads.
Performance Panel
Record a page load in the Performance panel. In the Experience track, red "Layout Shift" markers appear at each shift event. Click on any marker to see which elements shifted and by how much. The "Sources" section shows the specific DOM nodes that moved, helping you identify which font swap caused each shift.
CLS Breakdown in Lighthouse
Lighthouse reports the CLS score and identifies contributing elements. When fonts are the cause, you'll see text containers listed as shifting elements, often with timestamps matching your font load times from the Network panel.
Web Vitals Extension
The Web Vitals Chrome extension shows a real-time CLS counter and highlights shifting elements in red at the moment of shift. This is the fastest way to visually confirm font-related CLS on any page.
CSS size-adjust for Metric Matching
size-adjust is a CSS descriptor added to @font-face declarations to scale a font's metrics without changing its visual size. Applied to a fallback font declaration, it scales the fallback font to match the custom font's dimensions — so when the custom font swaps in, the text occupies the same space.
/* Custom font */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
/* Fallback with size-adjust to match Inter's metrics */
@font-face {
font-family: 'Inter-fallback';
src: local('Arial');
size-adjust: 107%;
font-weight: 400;
}
body {
font-family: 'Inter', 'Inter-fallback', Arial, sans-serif;
}
With this setup, Inter-fallback is a modified version of Arial scaled to 107% of its normal size — close to Inter's proportions. When Inter loads and swaps in, the text dimensions are nearly identical, and the layout barely shifts.
The size-adjust percentage isn't a guess — it's calculated from the ratio of the custom font's average character width to the fallback font's average character width. For Inter vs Arial, this ratio is approximately 107%. Different font pairs have different ratios.
Calculating the Right size-adjust Value
The formula for size-adjust is:
size-adjust = (custom_font_advance_width / fallback_font_advance_width) × 100%
In practice, you measure this by rendering a string in both fonts at the same font-size and comparing the rendered widths. A quick JavaScript approach:
function measureFontWidth(fontFamily, text = 'abcdefghijklmnopqrstuvwxyz') {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
ctx.font = `16px ${fontFamily}`;
return ctx.measureText(text).width;
}
const customWidth = measureFontWidth('Inter');
const fallbackWidth = measureFontWidth('Arial');
const sizeAdjust = (customWidth / fallbackWidth * 100).toFixed(2);
console.log(`size-adjust: ${sizeAdjust}%`);
Run this on a page where Inter is loaded and you have an accurate ratio. A more sophisticated approach measures multiple character sets and weights to produce a more representative average.
ascent-override, descent-override Descriptors
size-adjust addresses horizontal metrics (character width), but vertical metrics also affect layout. Different fonts have different ascent, descent, and line-gap values, which affect line height and the vertical space paragraphs occupy.
Three additional @font-face descriptors override these vertical metrics:
ascent-override: Overrides the font's ascent metricdescent-override: Overrides the font's descent metricline-gap-override: Overrides the font's line gap metric
@font-face {
font-family: 'Inter-fallback';
src: local('Arial');
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
font-weight: 400;
}
These values work together to make the fallback font render text blocks with the same overall dimensions as Inter. The paragraph takes the same number of lines. The line breaks occur in the same places. When Inter swaps in, the layout doesn't shift.
Getting Accurate Override Values
Calculating ascent, descent, and line-gap overrides manually is complex. The values depend on the font's internal metric tables (OS/2 and hhea tables in OpenType). Several approaches simplify this:
Using fonttools to inspect a font's metrics:
pip install fonttools
python3 -c "
from fontTools.ttLib import TTFont
font = TTFont('inter-regular.woff2')
os2 = font['OS/2']
head = font['head']
units_per_em = head.unitsPerEm
print(f'ascent: {os2.sTypoAscender}')
print(f'descent: {os2.sTypoDescender}')
print(f'line_gap: {os2.sTypoLineGap}')
print(f'units_per_em: {units_per_em}')
print(f'ascent_override: {(os2.sTypoAscender / units_per_em * 100):.2f}%')
print(f'descent_override: {(abs(os2.sTypoDescender) / units_per_em * 100):.2f}%')
print(f'line_gap_override: {(os2.sTypoLineGap / units_per_em * 100):.2f}%')
"
Apply these percentages as the override values in your fallback @font-face rule. The result is a fallback font that closely matches the vertical rhythm of your custom font, making swaps nearly invisible.
Automated Tools for Fallback Font Matching
Manually calculating size-adjust, ascent-override, descent-override, and line-gap-override for every font pair is tedious. Several tools automate this process.
fontaine (npm package)
fontaine analyzes a custom font and generates CSS overrides that minimize the metric difference between the custom font and common fallback fonts:
npm install -g fontaine
fontaine --font ./inter-regular.woff2 --fallback Arial
Output:
@font-face {
font-family: 'Inter Fallback: Arial';
src: local('Arial');
size-adjust: 107.04%;
ascent-override: 90.20%;
descent-override: 22.48%;
line-gap-override: 0%;
}
This CSS can be dropped directly into your stylesheet. The generated fallback font family name follows a convention that makes it easy to identify which custom font it matches.
Next.js Font Optimization
If you're using Next.js with the next/font module, fallback font metric matching is automatic:
import { Inter } from 'next/font/google';
const inter = Inter({
subsets: ['latin'],
display: 'swap',
});
Next.js generates the appropriate fallback @font-face with all override descriptors calculated from the font's actual metrics. The CSS is inlined into the page, so there are zero additional network requests. This is the best-practice approach for Next.js applications.
Capsize
The Capsize library (available as a JavaScript package and a web tool) calculates typographic metrics from a font file and generates CSS properties that produce predictable, layout-stable text rendering:
import { precomputeValues } from '@capsizecss/core';
import inter from '@capsizecss/metrics/inter';
import arial from '@capsizecss/metrics/arial';
const interFallback = precomputeValues({
fontSize: 16,
fontMetrics: inter,
lineGap: 0,
});
// Use the computed values to create a matching fallback
Capsize is more complex than fontaine but provides deeper control over metric matching and works well in design systems that need precise typographic control.
Browser DevTools Experimental Feature
Chrome DevTools has an experimental "Font Editor" panel that shows a font's ascent, descent, and line gap values visually. While it doesn't generate override CSS automatically, it provides a quick visual reference for understanding how two fonts' metrics compare.
Putting It All Together
A complete CLS-prevention setup for a site using Inter from Google Fonts:
/* 1. Load the custom font */
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-regular.woff2') format('woff2');
font-weight: 400;
font-display: swap;
}
/* 2. Create a metric-matched fallback */
@font-face {
font-family: 'Inter-Fallback';
src: local('Arial');
size-adjust: 107.04%;
ascent-override: 90.20%;
descent-override: 22.48%;
line-gap-override: 0%;
}
/* 3. Use both in the font stack */
body {
font-family: 'Inter', 'Inter-Fallback', Arial, sans-serif;
}
And in the HTML:
<!-- Preload Inter to minimize the window where fallback shows -->
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>
This combination — metric-matched fallback, preloaded font, and font-display: swap — produces font swaps that are nearly invisible to the human eye. The text may technically shift by a pixel or two during the swap, but the CLS score drops to near zero, and users experience no perceptible layout jump.
For zero-CLS font loading without this complexity, font-display: optional combined with preloading prevents swaps entirely at the cost of potentially showing the fallback font on first visit. Both approaches are valid; the metric-matching approach is better when brand consistency on first visit matters.
Common Font-Related CLS Patterns and Fixes
Certain page designs are particularly susceptible to font-induced CLS. Understanding the common patterns helps you anticipate and fix problems before they reach production.
Hero Section Text Reflow
The most impactful single CLS source is typically the above-the-fold hero section. If your hero uses a large heading in a custom display font, and the fallback font is substantially wider or taller, the entire page layout can shift when the custom font loads.
Fix: Preload the display heading font and use font-display: optional to prevent swapping, or use size-adjust to minimize the reflow when the swap occurs.
@font-face {
font-family: 'Playfair Display Fallback';
src: local('Georgia');
size-adjust: 115%;
ascent-override: 86%;
descent-override: 21%;
line-gap-override: 0%;
}
.hero-heading {
font-family: 'Playfair Display', 'Playfair Display Fallback', Georgia, serif;
}
Navigation Bar Height Changes
Navigation bars with inline text often change height when fonts load, pushing the entire page content down. A nav bar that's 48px tall with Arial might expand to 52px with a font that has a larger line height, causing all content below the nav to shift by 4px — small but enough to trigger a CLS violation.
Fix: Set an explicit height or min-height on your navigation element that accounts for the custom font's metrics, preventing any size change during font loading.
.nav {
height: 64px; /* Fixed height prevents font-induced CLS */
display: flex;
align-items: center;
}
Inline Elements Causing Reflow
Inline elements within paragraphs — links, inline code, emphasized text — can sometimes cause unexpected CLS when they use the same font as surrounding text but have a different computed size due to font metrics differences.
Monitor for small, surprising CLS events in DevTools using the Layout Shift Regions overlay. These small shifts are often caused by inline elements you wouldn't immediately suspect.
Testing Font CLS in CI
Manual DevTools inspection is useful during development, but automating CLS testing in your CI pipeline ensures regressions are caught before they reach production.
Playwright + Web Vitals
// tests/font-cls.spec.js
const { test, expect } = require('@playwright/test');
const { getCLS } = require('web-vitals');
test('font loading CLS is below threshold', async ({ page }) => {
let clsScore = 0;
await page.addInitScript(() => {
// Collect CLS from web-vitals
window.clsValues = [];
new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (!entry.hadRecentInput) {
window.clsValues.push(entry.value);
}
}
}).observe({ type: 'layout-shift', buffered: true });
});
await page.goto('https://yoursite.com');
// Wait for fonts to load
await page.waitForFunction(() => document.fonts.ready);
await page.waitForTimeout(2000); // Wait for any pending swaps
clsScore = await page.evaluate(() =>
window.clsValues.reduce((sum, v) => sum + v, 0)
);
expect(clsScore).toBeLessThan(0.1);
});
This test runs the page in a real browser, waits for fonts to load and swap, then measures the accumulated CLS. A failing test (CLS > 0.1) catches font metric changes, new font additions, or subsetting changes that impact layout stability.
Lighthouse CI
Lighthouse CI can enforce CLS budgets as part of your CI/CD pipeline:
{
"ci": {
"assert": {
"assertions": {
"cumulative-layout-shift": ["error", { "maxNumericValue": 0.1 }]
}
}
}
}
Run this on every pull request to prevent font-related CLS regressions from shipping to production.
Typography Terms
Try These Tools
Fonts Mentioned
Designed by Christian Robertson for Google's Material Design ecosystem, this neo-grotesque sans-serif is the most widely used typeface on the web and Android. Its dual-nature design balances mechanical precision with natural reading rhythm, making it equally at home in UI labels and long-form text. The variable font supports width and weight axes alongside Cyrillic, Greek, and extended Latin scripts.
Rasmus Andersson spent years refining this neo-grotesque specifically for computer screens, optimizing letter spacing, x-height, and stroke contrast for high readability at small sizes on digital displays. An optical size axis (opsz) lets the font automatically adjust its design for captions versus headlines, while the weight axis covers the full range from thin to black. It has become the de facto choice for dashboards, documentation sites, and developer tools worldwide.