---
title: Reduce CLS from font swapping
description: >-
  Use `@font-face` override descriptors (`size-adjust`, `ascent-override`,
  `descent-override`, `line-gap-override`) on fallback fonts to match web font
  metrics and minimize layout shift when the custom font finishes loading. This
  is an advanced technique best applied after addressing more common CLS causes
  like missing image dimensions.
source_url:
  html: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/reduce-cls-font-swapping
  md: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/reduce-cls-font-swapping.md
api_name: liquid
---

# Reduce CLS from font swapping

Use `@font-face` descriptors (`size-adjust`, `ascent-override`, `descent-override`, `line-gap-override`) on fallback fonts to match custom web font metrics and reduce layout shift during font swapping. This is an advanced technique for use after addressing more common CLS causes like missing image dimensions.

***

## Why

When you use `font-display: swap` in your `@font-face` declaration, the browser renders text immediately with a fallback system font, such as Arial or Times New Roman, instead of hiding it while the web font downloads. After the web font downloads, typically 100 to 500 ms after first paint, the browser swaps it in.

Shopify's [`font_face`](https://shopify.dev/docs/api/liquid/filters/font_face) filter doesn't add a `font-display` descriptor unless you pass one explicitly. Without it, browsers default to `font-display: auto`, which in Chrome behaves like `block`. Text is invisible for up to 3 seconds while the font loads (a Flash of Invisible Text, or FOIT). To get the swap behavior that this technique addresses, add `font_display: 'swap'` to your `font_face` call:

```liquid
{{ settings.type_header_font | font_face: font_display: 'swap' }}
```

If the web font has different character widths, line heights, or spacing than the fallback, then all text reflows at once, shifting surrounding content. This shift is recorded as [CLS](https://web.dev/cls/). On text-heavy pages, the cumulative shift from font swap can be 0.05 to 0.15 CLS units.

***

## How

### Set an explicit line-height first

Before you measure font metrics, set a unitless `line-height` on your text. A unitless value computes to a multiple of `font-size`, so the line box height doesn't depend on the font's own metrics and the swap can't change it:

```css
body {
  line-height: 1.5;
}
```

With the default `line-height: normal`, the line box comes from the font's ascent, descent, and line gap, so a swap between two fonts with different metrics changes the height of every line. That difference is proportional to `font-size`, which means headings shift more than body text does.

This handles the vertical half of the problem only. Different glyph widths can still change where lines wrap, which changes the line count and moves the content below. Use `size-adjust` for the width difference, and an explicit `line-height` for the height difference.

### Match fallback metrics to the web font

Use modern CSS [`@font-face` descriptors](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face) to adjust a local fallback font to more closely match the metrics of your web font, minimizing the visual shift when the swap occurs.

`@font-face` descriptors for fallback fonts:

* `size-adjust`: scales the glyphs of the fallback font.
* `ascent-override`: matches the height above the font's baseline.
* `descent-override`: matches the depth below the font's baseline.
* `line-gap-override`: matches the line spacing.

### Common fallback pairings

The following table lists common Shopify theme fonts and their closest system font fallbacks.

| Web font | Closest system fallback | Notes |
| - | - | - |
| Futura | Trebuchet MS | Geometric sans-serif, similar x-height. |
| Helvetica Neue | Arial | Near-identical metrics on most platforms. |
| Playfair Display | Georgia | Serif with similar proportions. |
| Montserrat | Verdana | Wide geometric sans-serif. |
| Lato | Tahoma | Humanist sans with similar metrics. |

There are no universal values. The correct `size-adjust`, `ascent-override`, `descent-override`, and `line-gap-override` values depend on the specific web font, the specific fallback font, and the weights and styles you use, so you have to measure them per font pair.

To measure them, read the metrics out of each font file and compute the ratios:

1. Get `unitsPerEm`, `ascender`, `descender`, `lineGap`, and the x-height or average character width for both the web font and the fallback. Font metric databases and open source tools such as [Fontaine](https://github.com/unjs/fontaine) and [`@capsizecss/metrics`](https://github.com/seek-oss/capsize) expose these values, and Fontaine can generate the fallback `@font-face` rule for you.
2. `size-adjust` is the ratio of the two fonts' average character widths, expressed as a percentage.
3. `ascent-override`, `descent-override`, and `line-gap-override` are the web font's `ascender`, `descender`, and `lineGap` divided by its `unitsPerEm`, then divided again by `size-adjust`, each expressed as a percentage.

Then verify by measuring: render the same block of text with the fallback and with the web font, and compare the rendered heights and widths in the **Elements** panel until they match.

Shipping a smaller font file shortens the window before the swap, but it doesn't change the CLS that the swap causes: the shift comes from the metric mismatch, not from the download time. Custom fonts that you upload to the theme's `assets/` directory are served byte for byte as uploaded, so subset them yourself, to the character sets your store actually renders, before you upload them.

***

## Examples

```css
/* In your main web font declaration, make sure font-display is swap */
@font-face {
  font-family: "Brand Font";
  src: url("/fonts/brand-font.woff2") format("woff2");
  font-display: swap;
}


/* Create a customized fallback font. Replace these values with ones
   measured for your own web font and fallback pair. */
@font-face {
  font-family: "Brand Font Fallback";
  src: local("Arial");
  size-adjust: 107%;
  ascent-override: 92%;
  descent-override: 24%;
  line-gap-override: 0%;
}


body {
  font-family: "Brand Font", "Brand Font Fallback", sans-serif;
}
```

Tooling:

* **Fallback font generators**: [Fontaine](https://github.com/unjs/fontaine) generates fallback `@font-face` rules from a font file, and [Capsize](https://github.com/seek-oss/capsize) exposes the underlying metrics if you want to compute the ratios yourself.
* **Chrome DevTools**: use the **Elements** panel **Computed** tab to compare the rendered dimensions of text with the fallback font and with the web font. This helps you visually assess how close your override values are.
* **Network throttling**: use browser developer tools to slow down the network, which makes the font swap easier to see.

When to prioritize:

* After you've already addressed other CLS sources, such as images without explicit dimensions and injected content without reserved space.
* If RUM data shows that font swapping is a primary contributor to your CLS.
* This is easier to implement in a custom theme than in a theme from the Shopify Theme Store.
* If your theme uses Shopify system fonts, this technique isn't needed because no font swap occurs. See [Eliminate font loading delays with system fonts](https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-system-fonts).

***

## Testing

* Use [Chrome DevTools Network panel](https://developer.chrome.com/docs/devtools/network) with throttling to visually inspect the font swap and measure CLS.
* Use the [Chrome DevTools Performance panel](https://developer.chrome.com/docs/devtools/performance) to record a trace and analyze layout shift events.
* Compare CLS scores from lab tests before and after the change.

***

## References

* [How to optimize Cumulative Layout Shift (CLS) on Shopify sites](https://performance.shopify.com/blogs/blog/how-to-optimize-cumulative-layout-shift-cls-on-shopify-sites)
* [Self-host web fonts on Shopify CDN](https://shopify.dev/docs/storefronts/themes/best-practices/performance/self-host-web-fonts)
* [Prevent image layout shift](https://shopify.dev/docs/storefronts/themes/best-practices/performance/prevent-image-layout-shift)
* [Reserve space for app-injected content](https://shopify.dev/docs/storefronts/themes/best-practices/performance/reserve-space-app-injected)

***
