Skip to main content

· Performance · 7 min read

Reduce Layout Shift from Web Fonts with size-adjust and ascent-override

Why text jumps when a web font loads, and how size-adjust, ascent-override, descent-override, and line-gap-override make the fallback take up the same space.

With font-display: swap, visitors can read your text straight away in a fallback font. Then the web font arrives, the text is redrawn, lines wrap in different places, and everything below moves. That jump counts toward Cumulative Layout Shift (CLS), one of the Core Web Vitals, and web.dev lists web fonts among the common causes in Optimize Cumulative Layout Shift.

You can make the jump much smaller by adjusting the fallback font so it takes up the same space as the web font. CSS has four @font-face descriptors for exactly that.

Why the swap moves things

Two fonts at the same font-size rarely take up the same space.

  • Width. If the fallback’s letters are narrower, a line holds more words. When the web font arrives, words move to the next line, the paragraph gets taller, and the content below shifts down.
  • Height. Each font defines how far it reaches above and below the baseline (its ascent and descent) and an extra line gap. With line-height: normal, those values set the height of every line, so a font with a taller ascent makes the whole block taller. Vertical metrics explained covers where these values come from.

The fix is to change the fallback, not the web font: scale its letters to a similar width and give it the same vertical metrics.

The four descriptors

All four go in an @font-face rule. MDN documents each one: size-adjust, ascent-override, descent-override, and line-gap-override.

  • size-adjust scales the font’s glyphs and metrics by a percentage, without changing the font-size you set in CSS.
  • ascent-override sets the height above the baseline, as a percentage of the font size.
  • descent-override sets the depth below the baseline.
  • line-gap-override sets the extra space between lines.

The usual pattern is a second @font-face that points at a font already installed on the visitor’s device with local(), adjusted to match your web font:

/* The web font */
@font-face {
  font-family: 'Brand Sans';
  src: url('/fonts/brand-sans-regular.woff2') format('woff2');
  font-weight: 400;
  font-style: normal;
  font-display: swap;
}

/* A local font, adjusted to take up the same space as Brand Sans.
   These numbers are placeholders: calculate them for your font. */
@font-face {
  font-family: 'Brand Sans Fallback';
  src: local('Arial');
  size-adjust: 105%;
  ascent-override: 90.48%;
  descent-override: 23.81%;
  line-gap-override: 0%;
}

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

While Brand Sans loads, the browser draws text with the adjusted Arial. When the web font arrives, the text changes shape but should stay in nearly the same place.

Calculating the values

Chrome’s Improved font fallbacks article gives the method. You need a few numbers from the font files:

  • Units per em (UPM): the size of the font’s design grid, often 1000 or 2048.
  • Ascent, descent, and line gap of the web font, in font units.
  • Average character width of both the web font and the fallback. This can only be approximated; Chrome’s article suggests weighting letters by how often they appear in your language.

Then:

size-adjust       = web font average width ÷ fallback average width
ascent-override   = ascent   ÷ (UPM × size-adjust)
descent-override  = descent  ÷ (UPM × size-adjust)
line-gap-override = line gap ÷ (UPM × size-adjust)

The overrides are divided by size-adjust because, according to the CSS Fonts Level 5 draft, size-adjust scales every metric of the font, including the values set by the override descriptors. Without the division, the vertical metrics would be scaled twice.

A worked example with made-up numbers for a hypothetical font, to show the arithmetic:

UPM 1000, ascent 950, descent 250, line gap 0
average width: web font is 5% wider than Arial → size-adjust = 105%

ascent-override   = 950 ÷ (1000 × 1.05) = 90.48%
descent-override  = 250 ÷ (1000 × 1.05) = 23.81%
line-gap-override =   0 ÷ (1000 × 1.05) = 0%

Those are the placeholder values in the CSS above. Your font’s real values will be different.

Different devices have different fallbacks

local('Arial') only works where Arial is installed. It is common on Windows and macOS, but not on Android, where Roboto is the usual sans-serif font. Chrome’s article handles this with one adjusted face per local font, each with its own values:

@font-face {
  font-family: 'Brand Sans Fallback';
  src: local('Arial');
  /* values calculated against Arial */
}

@font-face {
  font-family: 'Brand Sans Fallback Android';
  src: local('Roboto');
  /* values calculated against Roboto */
}

body {
  font-family: 'Brand Sans', 'Brand Sans Fallback', 'Brand Sans Fallback Android', sans-serif;
}

If a local font does not exist, the browser skips that face and moves on to the next name in the stack. The font fallback generator can write one rule per fallback for you, for example Arial for Windows, macOS, and iOS and Roboto for Android.

Let a tool do the math

Doing this by hand for every weight is tedious. Tools can read the metrics for you:

  • FontPivot’s font fallback generator reads your font file in the browser, calculates size-adjust and the three override values for fallbacks such as Arial, Roboto, or Times New Roman, and shows the shift with and without them before you copy the CSS.
  • Capsize provides font metrics and helpers for building fallback stacks.
  • Fontaine generates adjusted fallback @font-face rules automatically.
  • Frameworks can do it as part of the build. Next.js’s next/font adds adjusted fallbacks, and Astro’s Fonts API tries to generate optimized fallback fonts by default.

Converting a font from TTF or OTF to WOFF2 packs the same font tables, so metrics you calculate from the original also apply to the WOFF2 file you ship.

Browser support

At the time of writing, MDN’s compatibility data lists:

  • size-adjust: Chrome 92, Firefox 92, and Safari 17 and later.
  • ascent-override, descent-override, line-gap-override: Chrome 87 and Firefox 89 and later. Safari support appears only in Safari Technology Preview, not in a released version.

So in released versions of Safari, the width adjustment from size-adjust works, but the vertical overrides are ignored and some vertical movement can remain. Browsers that do not understand a descriptor simply skip it, so adding the overrides never breaks anything. Check Can I use for the current state.

Not the same as font-size-adjust

The font-size-adjust property sounds similar but works differently. It is set on elements, not in @font-face, and keeps the x-height (the height of lowercase letters) consistent when fonts change. It can help readability with mixed fonts, but it does not match ascent, descent, or width.

Adjusted fallbacks work best together with these:

  • Load the font sooner. If the web font arrives before the first paint, there is nothing to swap. Preloading the main text font can make that happen more often.
  • Use font-display: optional for body text. The browser uses the web font only if it is ready almost immediately, and never swaps later. See Choosing a font-display value.
  • Set a line height. A unitless line-height, such as 1.5, makes each line’s height independent of the font’s own vertical metrics. Width differences can still change where lines wrap.
body {
  line-height: 1.5;
}

Measure the result

  1. See the shift. In Chrome, open the Rendering panel in developer tools and turn on “Layout Shift Regions”. Reload with the cache disabled and a slow network preset. Areas that shift are highlighted.
  2. Compare with the fallback alone. Block the font file in the Network panel and take a screenshot, then unblock it and take another. With good overrides, line breaks and paragraph heights should be close.
  3. Check the number. Lighthouse and PageSpeed Insights report CLS for a test load. For real visitors, the web-vitals library can report CLS from the field.

Repeat for each weight you use. A Bold file has different widths from the Regular one, so it may need its own adjusted fallback.

Where FontPivot fits

Start with a web-ready file: convert a desktop font with TTF to WOFF2 or OTF to WOFF2, or use the font converter for other formats. The web font kit’s @font-face rule uses font-display: swap by default, and its style.css already includes adjusted fallback font rules for your font, calculated the same way as described above.

The font viewer shows a font’s name, weight, version, and characters, and its Vertical metrics panel lists the ascender, descender, and line gap from each of the font’s metric sets (hhea, typo, and win). That helps when you want to check the numbers yourself.

To get the CSS, use the font fallback generator. It measures your font’s metrics and average character width, writes the adjusted fallback @font-face rules and the font-family stack, and lets you compare the text with and without them in overlay, side-by-side, and swap views. As described above, released versions of Safari apply only the size-adjust part.

Further reading

Share:
Back to guides

Related guides

All guides »