unifont
  • All fonts Browse or filter the whole catalogue
  • Compare providers One family, every CDN, side by side
  • Documentation Install, providers, custom providers
  • HTTP API The endpoints this site is built on

Reducing layout shift

Use font metrics to build a fallback that takes up the same space as the real font.

In most cases, a webfont arrives after the first paint, so text is first set in whatever comes next in the stack. If that family is a different size, the page reflows when the real font lands. fontaine solves that issue: it generates a @font-face rule that points at a locally installed font and scales it with size-adjust, ascent-override, descent-override and line-gap-override until it occupies the same space as the real one.

unifont and fontaine are designed to work together. unifont resolves the location of the font and its descriptors; fontaine turns those into the fallback rule.

Let an integration do it

If you want fonts to work rather than to wire this up yourself, use an integration. Each of these resolves with unifont and generates the fallback ahead of time, so nothing runs in the browser.

In any Vite app (React, Svelte, Solid, Vue), add fontless:

ts

// vite.config.ts
import react from '@vitejs/plugin-react'
import { fontless } from 'fontless'
import { defineConfig } from 'vite'

export default defineConfig({
  plugins: [react(), fontless()],
})

Then name a family in your CSS. The plugin scans for it, resolves it through unifont, and injects the @font-face rules and the metric-matched fallback:

css

h1 {
  font-family: "Poppins", sans-serif;
}

In Nuxt, @nuxt/fonts does the same thing. You can enable it like this:

bash

npx nuxt add fonts

If you'd rather not add a plugin at all, Leturgerð is a CLI that downloads the files once and writes the @font-face rules and the fallback into your project:

bash

npx @leturgero/cli@latest

The result is plain font and CSS files that you commit, so it works with any tool rather than one bundler.

Build it yourself

If you're generating the CSS yourself, there are three steps: resolve the family, read the metrics of the file you're going to serve, and generate the fallback face.

ts

import { generateFontFace, readMetrics } from 'fontaine'
import { createUnifont, providers } from 'unifont'

const unifont = await createUnifont([providers.google()])
const { fonts, fallbacks } = await unifont.resolveFont('Poppins')

const url = fonts
  .flatMap(font => font.src)
  .find(source => 'url' in source)?.url

const metrics = url ? await readMetrics(url) : null

const css = metrics
  ? generateFontFace(metrics, { name: 'Poppins fallback', font: fallbacks[0] ?? 'sans-serif' })
  : ''

generateFontFace() expands the generic family for you: local("Arial"), local("Arimo"), … for sans-serif, so the rule matches something on most platforms. resolveFont() returns fallbacks for this reason. It's the generic the provider reports as closest to the family.

Put the fallback between the real family and the generic, so text is set in the adjusted face until the webfont loads:

css

h1 {
  font-family: "Poppins", "Poppins fallback", sans-serif;
}

Which metrics to use

readMetrics() downloads the font file and measures it, which costs a request per family. Two cheaper sources exist, with a tradeoff each:

  • getMetricsForFamily(family) from fontaine looks the name up in a bundled dataset. No network, but the dataset covers the family as published, which may differ from the cut a provider serves.
  • The metrics on a resolved FontFaceData, where the provider reports them. No network and no guessing, but few providers do, and the set is partial: generateFontFace() needs xWidthAvg to compute size-adjust, and a provider that omits it leaves the fallback unscaled.

Whichever you pick, cache the result. Metrics for a given URL never change, so they belong in the same storage as the rest of your font data.

When a fallback won't help

The adjusted face is built on local(), so it only applies if one of the named fonts is installed. Where nothing matches, the browser skips the rule and falls through to the generic, unadjusted. That's the usual outcome on a machine with no Microsoft or Apple system fonts, so treat the fallback as an improvement on most visits rather than a guarantee on all of them.

Next steps

  • Resolving fonts covers the options that control which faces you get back.
  • Caching covers where to keep resolved metadata between builds.