Web Development

Font Loading Without Layout Shift: Subsetting, size-adjust, Fallback

How to prevent layout shift from web fonts using subsetting, size-adjust CSS, and fallback metric overrides. Covers tooling, trade-offs, and gotchas.

Mohammed Saqib7 min read
A woman reads a book at a marble table with a laptop and coffee cup in a modern workspace.
Photo by https://kaboompics.com/ on Pexels · Pexels License

Web fonts cause layout shift because the browser doesn't know the metrics of a web font until it loads. When the font finally arrives and swaps in, the text reflows if the fallback font occupies a different amount of space. This reflow is what we measure as Cumulative Layout Shift (CLS), one of the Core Web Vitals alongside INP and LCP.

Why Web Fonts Cause Layout Shift

Cumulative Layout Shift is the unexpected movement of page content after the initial render. With web fonts, two common rendering strategies create this shift:

  • Flash of Invisible Text (FOIT) – The browser hides text until the web font loads, then swaps it in. If the hidden fallback has different metrics, the revealed text shifts the layout.
  • Flash of Unstyled Text (FOUT) – The browser shows the fallback font immediately, then swaps to the web font. Again, differing metrics cause a reflow.

The default font-display: block renders invisible text for up to 3 seconds, then swaps. Even font-display: swap (which shows fallback immediately) causes shift on swap if the two fonts aren't metrically identical.

The goal is simple: make the fallback font occupy exactly the same space as the web font, so no shift occurs when the swap happens. This requires three techniques working together: subsetting to reduce font load time, and metric overrides (via size-adjust, ascent-override, descent-override, line-gap-override) to match the fallback's metrics to the web font.

Subsetting: Reducing Payload and Improving Swap Timing

Subsetting removes unused glyphs from a font file. If your site only serves English text, you don't need Cyrillic, Greek, or CJK characters. Tools like glyphhanger (from Filament Group) or pyftsubset (from fonttools) can strip away everything except the characters you actually use.

Smaller font files load faster, especially over slow connections. This reduces the time before the real font is available, which in turn shortens the window during which layout shift can occur. With a subset font, you can often achieve a swap within the first few hundred milliseconds, minimising the chance of a CLS impact.

Trade-off: Over-aggressive subsetting can leave missing glyphs rendered as tofu (empty boxes). You must ensure your subset covers every character in your content, including punctuation, symbols (e.g., €, →), and any accented letters. A common mistake is subsetting only A–Z and forgetting about the en dash or curly quotes.

Example using pyftsubset:

pyftsubset Roboto-Regular.ttf \
  --unicodes="U+0020-007E,U+00A0,U+00A9,U+2013,U+2014,U+2018,U+2019,U+201C,U+201D,U+2026" \
  --output-file=Roboto-Regular.subset.woff2

This keeps only ASCII, non-breaking space, copyright, and common punctuation. Check your actual page content to expand the range.

The size-adjust Descriptor: Matching Fallback Metrics to the Web Font

The size-adjust CSS descriptor, defined in the @font-face rule for a fallback font, scales the entire font proportionally. It multiplies the font's ascent, descent, and line-gap metrics by the given percentage. The browser then uses these adjusted metrics for layout before the web font loads.

Example: if your web font has a larger x-height than Arial, you can shrink Arial to match:

@font-face {
  font-family: "Fallback for Roboto";
  src: local("Arial");
  size-adjust: 92%; /* Scale Arial down to approximate Roboto's metrics */
  ascent-override: 100%; /* Optional overrides applied after size-adjust */
  descent-override: 100%;
  line-gap-override: 0%;
}

The browser calculates the fallback's final metrics as size-adjust * original metric, then applies any explicit overrides. This descriptor alone often gets you close enough to prevent visible shift.

When to use it: If your web font and fallback have similar proportions (e.g., both sans-serif with comparable x-height), a single size-adjust value works well. For more divergent fonts, you need the finer controls.

Fallback Metric Overrides: ascent-override, descent-override, line-gap-override

When size-adjust isn't enough—for example, when the fallback has a significantly different cap-height or x-height—you can override individual metrics. These descriptors let you set explicit values for the ascent, descent, and line-gap, overriding the font's built-in metrics.

They are applied after size-adjust scaling. So if you set size-adjust: 90% and then ascent-override: 95%, the final ascent is 90% * 95% = 85.5% of the original.

Use cases:

  • A serif web font paired with a sans-serif fallback: the x-height mismatch may require adjusting ascent and descent separately.
  • A display font with very large ascenders: the fallback needs a different ascent override to avoid overlapping.

Example combining all overrides:

@font-face {
  font-family: "Fallback for Playfair Display";
  src: local("Georgia");
  size-adjust: 105%;
  ascent-override: 90%;
  descent-override: 110%;
  line-gap-override: 0%;
}

These overrides are supported in all modern browsers, but you should check caniuse for font-size-adjust (the descriptor name is size-adjust, but the broader feature falls under font-size-adjust). As of 2025, support is good across Chrome, Firefox, Safari, and Edge.

Generating Fallback Metrics with Tools

Manually tweaking percentages until the layout stops shifting is tedious. Tools like fontaine (npm package) automate the process. You provide the web font file(s) and a list of fallback font names (e.g., Arial, Helvetica). The tool computes the ideal size-adjust and metric overrides by comparing the web font's metrics to the system fallback.

Integration into a build pipeline:

  1. Subset your web font (e.g., with glyphhanger).
  2. Run fontaine on the subset font to generate a CSS file with the correct @font-face overrides.
  3. Include that CSS before your main stylesheet so the fallback metrics are defined early.

Example using fontaine in a Node.js script:

import { generateFallbackFontFace } from 'fontaine';
 
const result = generateFallbackFontFace({
  fontFamilyName: 'Roboto',
  src: 'local("Arial")',
  webFontFile: './src/fonts/Roboto-Regular.woff2',
});
 
console.log(result.css);
// Outputs something like:
// @font-face {
//   font-family: "Roboto fallback";
//   src: local("Arial");
//   size-adjust: 92.5%;
//   ascent-override: 100%;
//   descent-override: 100%;
//   line-gap-override: 0%;
// }

If you use Google Fonts, the API provides size-adjust hints for its fonts, but only for fonts served from Google. Self-hosting with computed overrides gives you full control.

Failure Modes and Gotchas

  • Browser support: Older browsers (e.g., Safari < 16.4, some Android browsers) don't support size-adjust or metric overrides. On those browsers, the fallback will cause layout shift. You can mitigate with feature detection or by using font-display: optional (which avoids swap entirely on unsupported browsers, but may show fallback permanently).
  • Variable fonts: Variable fonts can have inconsistent metrics across axes (e.g., weight, width). The override values computed for one axis may not hold for another. You may need to generate separate fallback faces for different axis values.
  • Color fonts: Fonts with color layers (e.g., emoji) often have metrics that don't match standard text fonts. Avoid using metric overrides for color fonts.
  • Caching: If you update the web font (e.g., a new version with different metrics) but forget to regenerate the fallback overrides, the fallback will no longer match, causing new CLS. Automate the regeneration in your CI/CD pipeline.
  • Subsetting and fallback failure: If the web font fails to load entirely, the browser will use the fallback. But if your subset doesn't cover all characters in the text, the fallback may render missing glyphs as tofu. Always ensure the fallback font (usually a system font) has full coverage.

Putting It All Together: A Build Pipeline

A robust pipeline for zero-CLS font loading looks like this:

  1. Subset your source font with glyphhanger or pyftsubset to produce a .woff2 file containing only the characters you use.
  2. Compute overrides with fontaine (or manually) to generate a CSS file with @font-face blocks for the fallback, including size-adjust and metric overrides.
  3. Declare the web font with font-display: swap (or optional if you prefer no swap on slow connections). The fallback @font-face must be declared first so the browser uses its adjusted metrics for layout.
  4. Serve the subset font with aggressive caching (see Cache-Control, ETags and stale-while-revalidate for Static Exports).
  5. Monitor CLS with a tool like Lighthouse or Web Vitals library to confirm no shift.

For Next.js projects, next/font handles subsetting and fallback overrides automatically. It's worth checking how it works under the hood—it essentially does what we described.

Example final CSS:

/* Fallback font with overrides */
@font-face {
  font-family: "Roboto fallback";
  src: local("Arial");
  size-adjust: 92.5%;
  ascent-override: 100%;
  descent-override: 100%;
  line-gap-override: 0%;
}
 
/* Web font with swap */
@font-face {
  font-family: "Roboto";
  src: url("/fonts/Roboto-Regular.subset.woff2") format("woff2");
  font-display: swap;
  font-weight: 400;
  font-style: normal;
}
 
body {
  font-family: "Roboto", "Roboto fallback", sans-serif;
}

The browser will first use "Roboto fallback" (with metrics adjusted to match Roboto), then swap to "Roboto" when it loads. No shift.

Key takeaways

  • Layout shift from web fonts is caused by metric differences between the fallback and web font. Use size-adjust and metric overrides to make them identical.
  • Subsetting reduces font file size and speeds up loading, shrinking the window for potential shift.
  • Tools like fontaine automate the computation of override values, making the process CI-friendly.
  • Always test on browsers that don't support metric overrides (e.g., older Safari) and consider font-display: optional as a fallback.
  • Keep the fallback font coverage complete in case the web font fails to load entirely.

Frequently asked questions

Does size-adjust work with variable fonts?
Yes, size-adjust works with variable fonts. However, because variable fonts can have different metrics at different axis values, the override values may need to be tuned for the specific axis instance you are using. Test across the range if your design uses multiple axis settings.
How do I subset fonts without breaking character coverage?
Always specify the Unicode ranges your content actually uses. Tools like glyphhanger can scan your HTML files to determine required characters. For dynamic content, include a broad range like Latin Extended, punctuation, and common symbols to avoid tofu in user-generated text.
Can I use font-display: optional with these techniques?
Yes, font-display: optional avoids layout shift entirely by never swapping if the font hasn't loaded after a brief timeout. Combining optional with size-adjust and metric overrides is unnecessary because the fallback is shown permanently. This is a valid choice if you prefer no web font over a delayed swap.
#fonts#css#performance#typography#cls
Share

Keep reading