fontaine
Automatic font fallback based on font metrics
Features
- 💪 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 |
Installation
With pnpm
Or, with npm
Or, with yarn
Usage
import { 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.
Category-Aware Fallbacks
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 } }
Default Category Fallbacks
- 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.
Fallback Priority
- 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'] } }
How it works
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'; }
💻 Development
- 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
Credits
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
License
Made with ❤️
Published under MIT License.