skip to content
Aaron Becker
The word 'typography' in paper cut-out letters taped to a white wall.

Fontsource, Fontaine, Tailwind and Vite

/ 10 min read

This one’s for the frontend typography nerds. Fontsource is a convenient way to add high-performance variable fonts to your site, and Fontaine can often dramatically reduce cumulative layout shift, improving user experience and performance metrics when using custom fonts. This article points out some common configuration pitfalls that can prevent the two packages from working together, especially when you add Tailwind to the mix.

Fontsource

Fontsource bundles Google Fonts into NPM packages that streamline the process of using custom fonts in your project. Tutorials on using Fontsource abound. Astro even includes one in its official docs; I’m not going to repeat it here. Custom fonts are generally a bad idea from a performance perspective, but the allure of control over your site’s typeface can be irresistible to certain personality types.

Fontaine

Fontaine helps mitigate an issue called Cumulative Layout Shift (CLS) that occurs when you use custom fonts on a web page. You typically want site visitors’ browsers to begin rendering content as soon as possible, which invovles using the font-display: swap CSS rule. This rule allows the browser to show content before your custom font is loaded, but once that font does become available, text might shift around as characters change size and aspect ratio.

CLS matters because Google considers it a “core web vital.” Apparently users find the rearrangement of page content jarring, especially if buttons or links move right before they’re clicked. More importantly, reducing CLS gives your site a better Lighthouse score, which can improve your site’s search rankings (all else equal). As with Fontsource, tutorials on using Fontaine are plentiful; I found this SvelteKit + Fontaine Tutorial by Rodney Lab particularly helpful.

fontaine and tailwind

Fontaine doesn’t look for fonts in your tailwind.config.ts file. Several tutorials I’ve come across assume that you can just list the custom font in your tailwind config file like this:

tailwind.config.ts (broken!)
import { fontFamily } from "tailwindcss/defaultTheme";
export default {
theme: {
extend: {
fontFamily: {
// older fontaine versions append "override" instead of "fallback"
sans: ['Geologica Variable', 'Geologica Variable fallback', ...fontFamily.sans],
},
},
},
}

Even if you include the correct fallback name, however, Fontaine isn’t going to actually generate the fallback @font-face declaration unless it finds a font-family declaration in a CSS file. This is a subtle bug because fallback fonts are only ever displayed for a few milliseconds before the custom font becomes available, so you might not notice whether Fontaine is working or not. Here’s how to work around this in a way that allows you to use the font-sans utility in your tailwind.config.ts file:

src/styles/global.css
:root {
/* fontaine will add fallbacks to any css variable with `font-family` in its name. This variable is applied to the `font-sans` utility in `tailwind.config.js` */
--sans-font-family: "Geologica Variable";
}
tailwind.config.ts
fontFamily: {
// fontaine will populate fallback into a css variable, defined in `global.css`
sans: ['var(--sans-font-family)', ...fontFamily.sans],
}

Fontsource + Fontaine

So now we get to the point of this article. While tutorials on either Fontsource or Fontaine are plentiful, you can run into some unexpected issues when using them together under a Vite-based framework like Astro or SvelteKit.

hashing it out with Vite

Vite is a build tool that bundles and versions your code and “static assets”, e.g. the parts of your site that aren’t code, like images, fonts, and CSS. It’s at the heart of both Astro and SvelteKit, my two favorite frontend frameworks.

Vite handles static assets gracefully: at build time, it adds a hash to the filename of each asset, which makes it possible to cache certain assets indefinitely. In this case: if a font file changes, your CSS style sheet will be updated to point to a new version. The user’s browser will download the new version linked from the updated CSS file and can store it until it changes again. No need to worry about the font file becoming stale since every change creates a distinct filename. This elegant technique is known as “cache busting”.

busted

Fontaine tutorials usually assume that you’re placing your font files somewhere under the public directory. Files in public are static, in the sense that this directory’s contents are treated as if they’re being served by a traditional web server like Apache or Nginx, but they’re not handled as “static assets” by Vite. This is convenient for tutorials that use Fontaine because the files in public have stable URLs that aren’t affected by cache busting.

If you install a font with a @fontsource NPM package, however, the font files are subject to Vite’s static asset handling. This means that if you have a @font-face declaration like this in a file like src/styles/global.css:

src/styles/global.css
/* geologica-latin-wght-normal */
@font-face {
font-family: 'Geologica Variable';
...
src: url(@fontsource-variable/geologica/files/geologica-latin-wght-normal.woff2) format('woff2-variations');
...
}

Then at build time, your CSS file will be inlined and Vite will bundle the font file to a new path that looks something like this:

/dist/_astro/geologica-latin-wght-normal.CNR-BNiN.woff2

The package-relative import url in global.css relies on Vite’s static asset handling to become a valid URL at build time. Fontaine doesn’t understand package-relative file import URls because they’re not really valid CSS syntax, they only work because Vite transforms them. This means that Fontaine will not be able to find font files at build time based on the import urls it finds in the CSS files in your project.

As a result Fontaine can’t upload your font files to the API it uses to retrieve font metrics, so it can’t calculate an optimized fallback font, so ultimately it doesn’t do anything. It doesn’t even throw an error, it just silently fails, which makes this a frustrating bug to debug.

the fix

Fontaine’s Vite plugin has a resolvePath option that gets us out of this mess. Tutorials usually use the resolvePath option to turn relative paths into absolute paths under the public directory. In this case, we can use the same option to point package-relative paths to the correct file locations under node_modules:

astro.config.ts
import { defineConfig } from "astro/config";
import { FontaineTransform } from 'fontaine';
import path from 'node:path';
export default defineConfig({
...
vite: {
...
plugins: [
...
FontaineTransform.vite({
// pick a fallback close to your custom font
fallbacks: ['Helvetica'],
resolvePath: (id) => {
// This function resolves the path to font files used in CSS `@font-face` rules.
// This assumes we're using urls relative to node packages in CSS `url()` functions,
// such that the `id` corresponds to a path under `node_modules` (it's the content of the `url()` function).
return new URL(path.join(path.dirname(import.meta.url), 'node_modules', id));
}
}),
],
}
});

conclusion

There you have it— a working example of using Fontsource and Fontaine together in an Astro project. The same approach has worked for me in SvelteKit projects and it should work for other Vite-based frameworks. The key is to use the resolvePath option to tell Fontaine how to find the font files it needs at build time by pointing it to the node_modules directory.

By applying Fontaine to this blog, I was able to reduce CLS for this very post (as measured by Lighthouse) from 0.454 to 0.064. This still takes a point off my lighthouse performance score, but 99 is way better than the 85 I had before using Fontaine. Don’t forget the important note above about the order of @font-face declarations; this is one of many ways you can trip yourself up while using Fontaine.

This approach minimizes asset loading waterfalls by allowing font definitions to be bundled with your global CSS file, while still allowing you to use the @fontsource package to retrieve optimized .woff2 font files. And it minimizes the need for aggressive font preloading, which can be a drag on performance. It’s a level of performance optimization that’s sure to appeal to a certain personality type.

fontaine alternative: preloading