Astro Font Fallbacks with Capsize

Astro Font Fallbacks with Capsize

Reduce Cumulative Layout Shift

Play this article

🚀 Font Fallbacks

In this Astro font fallbacks post, we see how you can use Capsize to reduce layout shift caused by font swaps. We focus on self-hosted fonts in vanilla CSS here, though you can apply the techniques elsewhere. Before tucking into the detail, we will unpack some terms just mentioned.

Font Swap

When you use web fonts or Google fonts on a site, it can take a moment for them to be downloaded to the user device, especially if they have a slow internet connection. By default, the user will see nothing, where the text should be, while the font gets downloaded in the background. To improve user experience, you will typically want to add the font-display: swap directive to your @fontface CSS. This tells the browser to display the text using a fallback font initially. That improves user experience as the visitor can start reading while the web font downloads. The browser swaps out the fallback for the web font, once it has downloaded it, so the page will ultimately look as the designer wanted it to.

Font Swap Layout Shift

We solve one user experience issue, but potentially introduce another. That is because not all fonts are the same size! When the browser switches from fallback to web font, there is typically some layout shift, just resulting from the two fonts having different metrics. This becomes more pronounced on narrower displays. Cumulative Layout Shift (CLS) is not great; it can be annoying if a button that you want to tap shifts just before your finger hits the screen.

One trick to reduce this font swap layout shift is to use modern CSS to adjust the metrics of the fallback font. Doing this, the fallback font will not look exactly as it should, but the shift can be dramatically reduced, or even eliminated on font swap. We look at techniques for doing that here, using Capsize.

Astro Font Fallbacks: Two browser windows side-by-side, one shows the fallback Times New Roman font while the other shows a sans font.  Despite the text being the same, layout is shifted.

😕 What is Capsize?

Capsize is a typography tool for text layouts. Creating precise text layouts can be frustrating, just from the way font sizing is defined. Capsize helps here, making your text height and the spacing above and below more predictable. The font metric data it uses here is also handy for working out the modern CSS adjustments we can make to a fallback font, so its layout approximates the web font’s.

An alternative to Capsize for reducing font swap layout shift is Fontaine. It has a Vite plugin, and is able automatically to update font face CSS (like we will do, using Capsize metrics). We do something a little more manual here with Capsize, just to help get a grounding in the issue and solution approach.

🧱 What are we Building?

We won’t build a site from start-to-finish. Instead, we will jump into snippets for a basic example with vanilla CSS and self-hosted fonts. There is a link to the complete code further down the page. You will be able to use that code to play around a little with font swaps and see the impact of our approach.

If you are new to working with self-hosted fonts in Astro, see the Astro Self-Hosted Fonts workflow video to get up to speed. If you want more background on font swapping and fallbacks, before continuing, see the thorough article by Katie Hempenius on Chrome Developers.

🌍 Astro Font Fallbacks: Global Styles

Our first code snippet is the global style sheet. The most important feature here is setting and using a custom CSS property for the font family:

:root {
    /* ...TRUNCATED */

    --font-family: 'Open Sans', sans-serif;

We will override that property in the font fallback CSS which we generate.

💄 font-face CSS

These are the font-face directives needed for our Open Sans font. Later, these CSS files will be imported into the Astro markup file.

/* open-sans-regular - latin */
@font-face {
    font-display: swap; /* Check for other options. */
    font-family: 'Open Sans';
    font-style: normal;
    font-weight: 400;
    src: url('/fonts/open-sans-v35-latin-regular.woff2') format('woff2'),
        /* Chrome 36+, Opera 23+, Firefox 39+ */ url('/fonts/open-sans-v35-latin-regular.woff')
            format('woff'); /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+ */

/* open-sans-700 - latin */
@font-face {
    font-display: swap; /* Check for other options. */
    font-family: 'Open Sans';
    font-style: normal;
    font-weight: 700;
    src: url('/fonts/open-sans-v35-latin-700.woff2') format('woff2'),
        /* Chrome 36+, Opera 23+, Firefox 39+ */ url('/fonts/open-sans-v35-latin-700.woff')
            format('woff'); /* Chrome 5+, Firefox 3.6+, IE 9+, Safari 5.1+ */

You can download woff files (and matching font-face directives) for most Google fonts with the google-webfonts-helper app. Note, font-face includes font-display: swap, which is important here.

📜 Capsize Script

Next, I added a TypeScript stand-alone script: capsize-font-fallbacks.ts in the project root directory. This uses node APIs to output an extra CSS stylesheet; convenient when working in vanilla CSS. You could, alternatively, use similar code as a utility function to export styles for use on a DOM element: more useful when working with a CSS-in-JS framework or something like vanilla-extract.

import { createFontStack } from '@capsizecss/core';
import arial from '@capsizecss/metrics/arial';
import openSans from '@capsizecss/metrics/openSans';
import roboto from '@capsizecss/metrics/roboto';
import { writeFileSync } from 'node:fs';
import path from 'node:path';

const __dirname = path.resolve();
const outputPath = path.join(__dirname, 'src/styles/font-fallbacks.css');

const { fontFaces, fontFamily } = createFontStack([openSans, arial, roboto]);

const css = `/* This file is generated using capsize. Run `pnpm vite-node capsize-font-fallbacks.ts` to refresh. */

:root {
  --font-family: ${fontFamily}, sans-serif;


writeFileSync(outputPath, css);

Capsize Tooling

You need to add two Capsize packages to your project:

pnpm add -D @capsizecss/core @capsizecss/metrics

The first package provides createFontStack, which we use to create a fallback font CSS directive and some font-face CSS:

const { fontFaces, fontFamily } = createFontStack(
  [openSans, arial, roboto]);

You can add {fontFaceFormat: 'styleObject'} as a second (options) parameter on createFontStack here, for output better suited to CSS-in-JS styling frameworks.

Choosing Fallback Fonts

Typically, you will want to use Arial as a fallback font for sans-serif fonts, and Times New Roman for serif. The Katie Hempenius article (mentioned above) notes, although these are quite safe fallbacks for desktop devices, neither is available on Android, so Roboto is a good second fallback.

You can import individual fonts from @capsizecss/metrics, as in lines 2-4. Then, combine them into a font stack array. The code adds sans-serif as a third fallback in the output CSS.


You can run TypeScript node scripts in your Astro project by installing vite-node as a dev dependency. To run vite-node (after install the package as a dependency), use:

pnpm vite-node capsize-font-fallbacks.ts

💅 Generated CSS

Running the script should create a src/styles/font-fallbacks.css file:

/* This file is generated with capsize. Run `vite-node capsize-font-fallbacks.ts` to refresh. */

:root {
  --font-family: "Open Sans", "Open Sans Fallback: Arial", "Open Sans Fallback: Roboto", sans-serif;

@font-face {
  font-family: 'Open Sans Fallback: Arial';
  src: local('Arial');
  ascent-override: 101.1768%;
  descent-override: 27.7323%;
  size-adjust: 105.6416%

@font-face {
  font-family: 'Open Sans Fallback: Roboto';
  src: local('Roboto');
  ascent-override: 101.2887%;
  descent-override: 27.763%;
  size-adjust: 105.5249%

For the first time, you get to see these mysterious CSS font adjustments (ascent-override, descent-override and size-adjust)!

🏡 Homepage

The final missing part is the client code for the home page, which will include all the stylesheets we mentioned. An alternative is to place those imports in a layout file, accessible to all site pages.

import '~styles/fonts.css';
import '~styles/global.css';

import '~styles/font-fallbacks.css';

<html lang="en-GB">
        < !-- TRUNCATED... -->

            <h1>🌟 Twinkle, Twinkle, Little Star</h1>
                Twinkle, twinkle, little star,<br />
                How I wonder what you are! <br />
                Up above the world so high, <br />
                Like a diamond in the sky.
            < !-- TRUNCATED... -->

💯 Astro Font Fallbacks: Frontend

When you have everything working, try commenting out the fallback CSS import line in your Astro page file to see the difference. You will probably need to throttle the network speed to see the load in detail.

I used Times New Roman as a fallback for a sans-serif font here, just to exaggerate the layout shift. Generally, you will use opt for Arial as a safe sans-serif fallback.

🙌🏽 Astro Font Fallbacks: Wrapping Up

In this post, we saw a way to implement Astro font fallbacks. In particular, we saw:

  • why using a font-display: swap improves user experience;

  • why you might use Capsize or Fontaine to reduce CLS; and

  • how you can use Capsize to generate fallback CSS.

You can see the full code for this project in the Rodney Lab GitHub repo. I do hope you have found this post useful! Let me know if you decide to use Capsize in a production project, or if you preferred Fontaine. Also, let me know about any possible improvements to the content above.

🙏🏽 Astro Font Fallbacks: Feedback

Have you found the post useful? Would you prefer to see posts on another topic instead? Get in touch with ideas for new posts. Also, if you like my writing style, get in touch if I can write some posts for your company site on a consultancy basis. Read on to find ways to get in touch, further below. If you want to support posts similar to this one and can spare a few dollars, euros or pounds, please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter, on Mastodon and also the #rodney Element Matrix room. Also, see further ways to get in touch with Rodney Lab. I post regularly on Astro as well as SEO. Also, subscribe to the newsletter to keep up-to-date with our latest projects.

Did you find this article valuable?

Support Rodney Lab - Content Site WebDev & Serverless by becoming a sponsor. Any amount is appreciated!