A selfie of me hiking on snowy Mt. Rainier. I am smiling while wearing my hat and sunglasses. Behind my head, in the distance, you can see the looming peak of Mt. Adams.
0 min read
Published · Updated

Reduce font-related CLS in your Astro projects with Fontaine

A Vite plugin that makes `font-display: swap` actually viable.

You want to use font-display: swap or font-display: auto in your project’s CSS to boost your page load­ing speed. But when you do, this hap­pens:

A visual demonstration of cumulative layout shift caused by font-display swap.

(Image cour­tesy: js-​craft.io)

Ugh! That an­noy­ing shift when your font files do load con­tributes to the cu­mu­la­tive lay­out shift (CLS) of your page. The more CLS there is, the more points will be de­ducted from your Google Light­house score. Bum­mer!

Why Fontaine?

Fontaine is a Vite plu­gin writ­ten by Daniel Roe that helps you op­ti­mize a fall­back font. The plu­gin uses Cap­size to an­a­lyze your type­face and cre­ate font met­rics for as­cen­ders and de­scen­ders ac­cord­ingly. Those are fancy-​schmancy CSS fea­tures that are de­cently sup­ported across browsers (just wait­ing on We­bkit 😐). Using this tech­nique, a fall­back sys­tem font will oc­cupy about the same vi­sual space as your pre­ferred type. When your cus­tom type does fi­nally swap in, there won’t be a jar­ring lay­out shift. Pretty nifty, huh?

Get­ting started

Let’s get this added to an Astro project! In my ex­am­ple, I will be using Tail­wind CSS, but you cer­tainly don’t have to. Feel free to use whichever CSS frame­work (or just plain ol’ CSS) your heart de­sires.

Let’s cre­ate a bare-​bones Astro project with Tail­wind CSS. To do that, we’ll run this com­mand:

pnpm create astro@latest -- --template with-tailwindcss

Next, we’ll add in the Fontaine pack­age:

pnpm add fontaine

Add your font files

Now, we’ll want to add our font files to Astro’s /public di­rec­tory. This di­rec­tory is where sta­tic as­sets that won’t be processed by Astro live. I like to keep my font files or­ga­nized by type­face and file type, but feel free to do what­ever floats your boat.

your-astro-project/
├─ public/
│  ╰─ fonts/
│     ╰─ SpaceGrotesk/
│        ╰─ ttf/
│           ╰─ SpaceGrotesk-Regular.ttf

You can use Google Fonts or Adobe Type­kit — but host­ing your own font files is pre­ferred for a litany of rea­sons. If you do choose Google Fonts or Type­kit, you will need to ad­just the Fontaine URL in the Astro con­fig ac­cord­ingly.

Now we’ll need to write some @font-face rules to tell our browser where to lo­cate our font files. If you’re writ­ing with SCSS, you can use this handy func­tion I wrote to load in a bunch of font files cleanly.

fonts.css
@font-face {
	font-family: "Space Grotesk";
	src: url("/fonts/SpaceGrotesk/SpaceGrotesk-Regular.ttf") format("truetype");
	font-style: normal;
	font-weight: 400;
	font-display: swap;
	unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F,
		U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
}

Once you’ve writ­ten this CSS, you will need to im­port it in an Astro file so that your project will ac­tu­ally use it. If you use a base <head /> com­po­nent, that would be a good spot; oth­er­wise, a lay­out file would work, too.

Con­fig files

We need to do two final things to get this to work. First, we need to ref­er­ence our cus­tom type in our tailwind.config.cjs file. What I gen­er­ally do is ex­tend the de­fault sans-​serif font stack. You can do that like so:

tailwind.config.cjs
const defaultTheme = require("tailwindcss/defaultTheme")

module.exports = {
	theme: {
		extend: {
			fontFamily: {
				sans: ["Space\\ Grotesk", ...defaultTheme.fontFamily.sans],
			},
		},
	},
}

Ex­cel­lent! Now, we’re going to tell Tail­wind to try our backup type­face if our pri­mary one isn’t avail­able. By de­fault, the specially-​adjusted Fontaine font is the name of your type­face ap­pended with override. So we will ref­er­ence that in our Tail­wind like so:

sans: ["Space\\ Grotesk", "Space\\ Grotesk\\ override", ...defaultTheme.fontFamily.sans]

In our astro.config.mjs file, we will add the Fontaine plu­gin and give it our con­fig. With Fontaine, you can spec­ify many font fall­backs, but only the first in your array will be used for the ad­just­ment cal­cu­la­tions. Be­cause we al­ready have fall­backs spec­i­fied in our Tail­wind con­fig, there’s re­ally only value in using one fall­back font. I choose Arial for sans-​serif type­faces be­cause it’s pretty much guar­an­teed to be in­stalled on any ma­chine.

We tell Fontaine where to lo­cate our font files like this:

astro.config.js
import { defineConfig } from "astro/config"
import { FontaineTransform } from "fontaine"

export default defineConfig({
	vite: {
		plugins: [
			FontaineTransform.vite({
				fallbacks: ["Arial"],
				resolvePath: (id) => new URL(`./public${id}`, import.meta.url), // id is the font src value in the CSS
			}),
		],
	},
})

And that’s it! If we’ve done every­thing cor­rectly, we should be able to run our build com­mand error-​free:

pnpm build

You can check to make sure Fontaine is work­ing by in­spect­ing the /dist folder where Astro spits out com­pleted builds. In the /dist folder, there’s an _astro di­rec­tory, and in­side of that there should be a .css file. Open­ing it, you should see a de­c­la­ra­tion like this at the start of the file (mine was mini­fied):

@font-face {
	font-family: Space Grotesk override;
	src: local("Arial");
	ascent-override: 98.4%;
	descent-override: 29.2%;
	line-gap-override: 0%;
}

Voilà ! We have suc­cess.

Mixed with a cou­ple of other font load­ing strate­gies — prefetch­ing font files, sub­set­ting for Latin al­pha­bets, self-​hosting your font files, caching your font files — we can also use font-display: swap or font-display: auto with­out hav­ing to worry about jar­ring lay­out shifts (on non-​Webkit browsers).

Work­ing ex­am­ple

I would love to hear your thoughts and feed­back, feel free to drop me a line or tweet at me. I am al­ways open to im­prove­ments to this tu­to­r­ial or my code ex­am­ple.

Until next time, au revoir !


Fur­ther read­ing

Interested in working together?

Reach out to learn more about me and/or my work.