Automatic font fallback based on font metrics
- 💪 Reduces CLS by using local font fallbacks with crafted font metrics.
- ✨ Generates font metrics and overrides automatically.
- ⚡️ Pure CSS, zero runtime overhead.
On the playground project, enabling/disabling fontaine makes the following difference rendering /, with no customisation required:
| Before | After | |
|---|---|---|
| CLS | 0.24 |
0.054 |
| Performance | 92 |
100 |
With pnpm
pnpm add -D fontaineOr, with npm
npm install -D fontaineOr, with yarn
yarn add -D fontaineimport { FontaineTransform } from 'fontaine'
// Astro config - astro.config.mjs
import { defineConfig } from 'astro/config'
const options = {
// You can specify fallbacks as an array (applies to all fonts)
fallbacks: ['BlinkMacSystemFont', 'Segoe UI', 'Helvetica Neue', 'Arial', 'Noto Sans'],
// Or as an object to configure specific fallbacks per font family
// fallbacks: {
// Poppins: ['Helvetica Neue'],
// 'JetBrains Mono': ['Courier New']
// },
// You may need to resolve assets like `/fonts/Roboto.woff2` to a particular directory
resolvePath: id => `file:///path/to/public/dir${id}`,
// fallbackName: (originalName) => `${name} fallback`
// sourcemap: false
// skipFontFaceGeneration: (fallbackName) => fallbackName === 'Roboto fallback'
}
// Vite
export default {
plugins: [FontaineTransform.vite(options)]
}
// Next.js
export default {
webpack(config) {
config.plugins = config.plugins || []
config.plugins.push(FontaineTransform.webpack(options))
return config
},
}
// Docusaurus plugin - to be provided to the plugins option of docusaurus.config.js
// n.b. you'll likely need to require fontaine rather than importing it
const fontaine = require('fontaine')
function fontainePlugin(_context, _options) {
return {
name: 'fontaine-plugin',
configureWebpack(_config, _isServer) {
return {
plugins: [
fontaine.FontaineTransform.webpack(options),
],
}
},
}
}
// Gatsby config - gatsby-node.js
const { FontaineTransform } = require('fontaine')
exports.onCreateWebpackConfig = ({ stage, actions, getConfig }) => {
const config = getConfig()
config.plugins.push(FontaineTransform.webpack(options))
actions.replaceWebpackConfig(config)
}
export default defineConfig({
integrations: [],
vite: {
plugins: [
FontaineTransform.vite({
fallbacks: ['Arial'],
resolvePath: id => new URL(`./public${id}`, import.meta.url), // id is the font src value in the CSS
}),
],
},
})Note If you are using Nuxt, check out nuxt-font-metrics which uses
fontaineunder the hood.
If your custom font is used through the mechanism of CSS variables, you'll need to make a tweak to your CSS variables to give fontaine a helping hand. Docusaurus is an example of this, it uses the --ifm-font-family-base variable to reference a custom font. In order that fontaine can connect the variable with the font, we need to add a {Name of Font} fallback suffix to that variable. What does this look like? Well imagine we were using the custom font Poppins which is referenced from the --ifm-font-family-base variable, we'd make the following adjustment:
:root {
/* ... */
- --ifm-font-family-base: 'Poppins';
+ --ifm-font-family-base: 'Poppins', 'Poppins fallback';Behind the scenes, there is a 'Poppins fallback' @font-face rule that has been created by fontaine. By manually adding this fallback font family to our CSS variable, we make our site use the fallback @font-face rule with the correct font metrics that fontaine generates.
Fontaine automatically selects appropriate fallback fonts based on font categories (serif, sans-serif, monospace, etc.) when using object-based fallback configuration.
const options = {
// Use an empty object to enable automatic category-based fallbacks
fallbacks: {},
// Or customize specific categories while keeping defaults for others
categoryFallbacks: {
'serif': ['Georgia', 'Times New Roman'],
'sans-serif': ['Arial', 'Helvetica'],
// monospace, display, and handwriting categories use defaults
}
}- sans-serif:
BlinkMacSystemFont,Segoe UI,Helvetica Neue,Arial,Noto Sans - serif:
Times New Roman,Georgia,Noto Serif - monospace:
Courier New,Roboto Mono,Noto Sans Mono - display & handwriting: Same as sans-serif
Note: These presets are available programmatically via
DEFAULT_CATEGORY_FALLBACKSand can be used with theresolveCategoryFallbackshelper function for advanced use cases. Both are exported from thefontainepackage and shared across related packages (e.g.,fontless) to ensure consistent fallback behavior.
- Array format (
fallbacks: ['Arial']) - Uses specified fonts for all families (legacy behavior) - Per-family override (
fallbacks: { Poppins: ['Arial'] }) - Uses specified fonts for that family - Category-based - When a family isn't specified, uses the appropriate category preset
- Global default - Falls back to sans-serif preset if no category is detected
Example:
{
fallbacks: {
// Specific override for Poppins
'Poppins': ['Arial'],
// Other sans-serif fonts will use the sans-serif preset
// Serif fonts will use the serif preset automatically
},
categoryFallbacks: {
// Customize the serif preset
'serif': ['Georgia']
}
}fontaine will scan your @font-face rules and generate fallback rules with the correct metrics. For example:
@font-face {
font-family: 'Roboto';
font-display: swap;
src: url('/fonts/Roboto.woff2') format('woff2'), url('/fonts/Roboto.woff')
format('woff');
font-weight: 700;
}
/* This additional font-face declaration will be added to your CSS. */
@font-face {
font-family: 'Roboto fallback';
src: local('BlinkMacSystemFont'), local('Segoe UI'), local('Helvetica Neue'),
local('Arial'), local('Noto Sans');
ascent-override: 92.7734375%;
descent-override: 24.4140625%;
line-gap-override: 0%;
}Then, whenever you use font-family: 'Roboto', fontaine will add the fallback to the font-family:
:root {
font-family: 'Roboto';
/* This becomes */
font-family: 'Roboto', 'Roboto fallback';
}- Clone this repository
- Enable Corepack using
corepack enable(usenpm i -g corepackfor Node.js < 16.10) - Install dependencies using
pnpm install - Run interactive tests using
pnpm dev; launch a vite server using source code withpnpm demo:dev
This would not have been possible without:
- amazing tooling and generated metrics from capsizecss
- suggestion and algorithm from Katie Hempenius & Kara Erickson on the Google Aurora team - see notes on calculating font metric overrides
- package name suggestion from @clemcode
Made with ❤️
Published under MIT License.