🛡️ Font Fallbacks Strategy

When a custom web font loads, it replaces the system font. If the two fonts have different widths or x-heights, the text reflows, causing a Cumulative Layout Shift (CLS). A good fallback strategy minimizes this shift by tuning the system font to match the custom font's dimensions.

1. The Problem: Cumulative Layout Shift from Font Swap

When a custom web font loads and replaces the system fallback, the two fonts almost never have identical metrics. The result: text reflows — paragraphs expand, shrink, or shift vertically. This is a Cumulative Layout Shift (CLS) event, which harms both user experience and Google Core Web Vitals scores.

Imagine your custom font is wider than Arial. When it loads, a paragraph might expand from 3 lines to 4 lines, pushing all content below it down. This is jarring for users and can cause them to accidentally tap the wrong link as the page moves under their finger.

System Font (Arial, before load)
The quick brown fox jumps over the lazy dog. Arial is narrower, so this fits in 2 lines.
Custom Font (Inter, after load)
The quick brown fox jumps over the lazy dog. Inter is slightly wider, pushing text to 3 lines — shifting everything below downward.

For Psilobase, where users may be reading detailed harm-reduction information or safety checklists, a sudden layout jump is especially disruptive. A well-tuned fallback strategy eliminates the swap jump entirely.

2. The Solution: CSS Font Metric Override Descriptors

Modern CSS provides four @font-face descriptors that adjust how a fallback font renders to match your custom font's metrics:

  • size-adjust: Scales the entire fallback font up or down by a percentage.
  • ascent-override: Adjusts the height above the baseline where capitals and ascenders sit.
  • descent-override: Adjusts the depth below the baseline for descenders (g, p, y).
  • line-gap-override: Adjusts the built-in line spacing suggestion. Most modern fonts set this to 0.
/* Define a calibrated fallback that matches Inter */
@font-face {
  font-family: 'Inter-fallback';
  src: local('Arial');
  ascent-override: 90%;
  descent-override: 22%;
  line-gap-override: 0%;
  size-adjust: 107%;  /* Scale Arial up to approximate Inter's width */
}

/* Apply in the font stack */
body {
  font-family: 'Inter', 'Inter-fallback', -apple-system, sans-serif;
}

/* Load the actual custom font */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-var.woff2') format('woff2');
  font-weight: 100 900;  /* Variable font covers all weights */
  font-display: swap;    /* Show fallback immediately, swap when loaded */
}

3. Finding the Right Metric Values

Guessing the correct values is impractical. Use one of these systematic approaches:

Font Style Matcher (Manual)

The Font Style Matcher tool overlays your custom font on the system fallback with interactive sliders for each descriptor. Adjust until the text aligns perfectly, then copy the resulting CSS values. Available at meowni.ca/font-style-matcher.

Automatic via next/font

If the site uses Next.js, the next/font module calculates and applies fallback overrides automatically:

import { Inter } from 'next/font/google';
const inter = Inter({ subsets: ['latin'], display: 'swap' });
// next/font generates the correct override @font-face automatically

fontaine Package (Vite/Astro/Nuxt)

The fontaine package by UnJS reads font metrics from the binary file and auto-generates calibrated overrides:

npm install fontaine
# Then in vite.config.ts:
import { fontaine } from 'fontaine';
export default { plugins: [fontaine()] };

Canvas Measurement (Manual Code)

function measureFont(fontFamily, text = 'Gg') {
  const canvas = document.createElement('canvas');
  const ctx = canvas.getContext('2d');
  ctx.font = `100px ${fontFamily}`;
  const m = ctx.measureText(text);
  return { ascent: m.actualBoundingBoxAscent,
           descent: m.actualBoundingBoxDescent,
           width: m.width };
}
const inter = measureFont('Inter');
const arial = measureFont('Arial');
console.log('size-adjust:', (inter.width / arial.width * 100).toFixed(2) + '%');

4. The font-display Strategy

The font-display descriptor controls what happens while the custom font loads. Choosing correctly minimises both invisible text (FOIT) and layout shift (CLS).

  • swap: Show the fallback immediately; swap when the custom font loads. One layout shift, but no invisible text. Best for body copy — readability is prioritised over visual perfection.
  • fallback: A brief block period (~100ms), then swap. Good for heading fonts where the custom font significantly improves aesthetics.
  • optional: Only use the custom font if it loads within a very short window. Otherwise stay on the fallback for the entire session. Best performance score, but inconsistent branding.
  • block: Hide text for up to 3 seconds. Avoid for body text — causes prolonged invisible content on slow connections.

5. Standard Fallback Stacks

Always include a graceful chain ending with a generic family. This ensures basic readability even when all custom fonts fail to load.

Sans-Serif (Body Text)

font-family:
  'Inter',              /* Custom web font */
  'Inter-fallback',     /* Calibrated system font override */
  -apple-system,        /* San Francisco on macOS/iOS */
  BlinkMacSystemFont,   /* Chrome on macOS */
  'Segoe UI',           /* Windows */
  Roboto,               /* Android */
  Helvetica,
  Arial,
  sans-serif;           /* Generic — required */

Serif (Headings or Long-Form)

font-family:
  'Merriweather',
  'Merriweather-fallback',
  Georgia,
  Cambria,
  'Times New Roman',
  Times,
  serif;

Monospace (Code Blocks)

font-family:
  'Fira Code',
  'Cascadia Code',
  'JetBrains Mono',
  'Courier New',
  Courier,
  monospace;

Zero-Cost System UI Stack

For maximum performance with no network requests:

font-family: ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont,
  'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;

6. Font Subsetting for Performance

Reducing font file size is equally important. A full Inter variable font is ~350KB; a Latin-subset WOFF2 is 30–60KB.

  • Subset to Latin characters using unicode-range. Psilobase is English-only.
  • WOFF2 only — all modern browsers support it. No TTF, EOT, or SVG fallbacks needed.
  • Preload critical weights with <link rel="preload"> in the <head> to start loading before CSS parses.
<!-- Preload the most critical font file -->
<link rel="preload" href="/fonts/inter-400.woff2"
  as="font" type="font/woff2" crossorigin="anonymous">

/* In CSS — restrict to Latin range */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-400.woff2') format('woff2');
  font-weight: 400;
  font-display: swap;
  unicode-range: U+0020-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;
}

📅 Created: 25 January 2026 | ✍️ Author: Typography Team | 🔄 Next Review: July 2026