Skip to content

Astro Font Optimization

Basalt UI ships @fontsource-variable packages as peer dependencies so fonts work in any framework. In Astro apps you can go further: the Astro 6 Fonts API downloads fonts at build time, serves them from /_astro/fonts/ with content-hashed filenames, and auto-injects <link rel="preload"> so the browser fetches fonts before the first paint.

The result: zero FOUT on first load, no third-party font requests, and automatic fallback metric adjustments (the role previously filled by vite-plugin-fontaine).


Section titled “Option A — Astro 6 Fonts API (Recommended)”

Requires Astro 6+. The Fonts API is stable — no experimental flag needed.

Terminal window
npx @astrojs/upgrade
import { defineConfig, fontProviders } from 'astro/config'
export default defineConfig({
fonts: [
{
provider: fontProviders.fontsource(),
name: 'Instrument Sans', // Fontsource registry name, not the CSS font-family
cssVariable: '--font-instrument-sans',
weights: [400, 500, 600, 700],
styles: ['normal'],
},
{
provider: fontProviders.fontsource(),
name: 'JetBrains Mono', // Fontsource registry name
cssVariable: '--font-jetbrains-mono',
weights: [400, 500, 700],
styles: ['normal'],
},
],
})

fontProviders.fontsource() fetches font files from the Fontsource registry during the build and caches them locally — no @fontsource-variable/* npm packages needed in the Astro app itself.

---
import { Font } from 'astro:assets'
---
<head>
<Font cssVariable="--font-instrument-sans" preload />
<Font cssVariable="--font-jetbrains-mono" preload />
</head>

preload injects <link rel="preload" as="font"> tags for the configured font files. To preload only specific weights, styles, or subsets, pass an array of filter objects instead: preload={[{ weight: '400', style: 'normal' }]}. Remove the attribute entirely to skip preloading (useful for secondary/icon fonts).

The API injects a --font-instrument-sans CSS variable containing the font-family value. Basalt UI’s index.css already maps this to the --font-sans token via:

@theme inline {
--font-sans: var(--font-instrument-sans, 'Instrument Sans Variable', sans-serif);
--font-mono: var(--font-jetbrains-mono, 'JetBrains Mono Variable', monospace);
}

When the Fonts API is active the variable resolves to the build-time hashed font stack; when it is absent (e.g. non-Astro consumers) the fallback chain kicks in.

If you previously used vite-plugin-fontaine, remove it — the Fonts API generates equivalent metric fallbacks natively:

Terminal window
bun remove fontaine

Option B — Manual Preloads + Fontaine (Astro 4 / 5)

Section titled “Option B — Manual Preloads + Fontaine (Astro 4 / 5)”

Use this approach when you cannot upgrade to Astro 6.

Terminal window
bun add -D fontaine
astro.config.mjs
import { FontaineTransform } from 'fontaine'
export default defineConfig({
vite: {
plugins: [
FontaineTransform.vite({
fallbacks: {
'Instrument Sans Variable': ['Helvetica Neue', 'Segoe UI', 'Arial'],
'JetBrains Mono Variable': ['Consolas', 'Menlo', 'Courier New'],
},
resolvePath: (id) => new URL(`../../node_modules/${id}`, import.meta.url),
}),
],
},
})

Find the exact hashed woff2 path from the built dist/_astro/ output and hardcode it. The path below is illustrative only:

<link
rel="preload"
as="font"
type="font/woff2"
href="/_astro/instrument-sans-latin-400-normal.<hash>.woff2"
crossorigin
/>

Caveat: Filenames are content-hashed and will change when font versions update. You must re-check the path after every @fontsource-variable upgrade.


The weights array in the config controls which static instances are downloaded. For variable fonts you only need one entry per style (the range is encoded in the woff2 file). If you see a specific weight missing, verify the font file covers that range via the Fontsource registry.

fontProviders.fontsource() resolves font names from the Fontsource registry at build time. The name field must match the Fontsource registry family name, not the CSS font-family value. Use "Instrument Sans" (the registry name), not "Instrument Sans Variable" (the CSS family). Check the font’s metadata.json"family" field for the exact value.