Type something to search...
How to load custom fonts efficiently in Astro?

How to load custom fonts efficiently in Astro?

The most efficient way to load custom fonts in Astro is the built-in Fonts API, which has been stable since Astro 6. You register each font in the top-level fonts array of astro.config.mjs with a name, a cssVariable and a provider such as fontProviders.local() or fontProviders.fontsource(). Then you add <Font cssVariable="--font-inter" /> from astro:assets to your page head and use font-family: var(--font-inter) in CSS. Astro downloads the files at build time, serves them from your own domain, writes the @font-face rules, generates metric-adjusted fallback fonts to reduce layout shift, and can add preload links for the files you choose.

Before this API existed, Astro sites usually loaded fonts from Google's CDN or hand-wrote @font-face rules for self-hosted files. Both still work, but the Fonts API gives you the performance best practices by default. In this article you'll learn how to configure local fonts and provider fonts, apply them with the <Font /> component, preload only what matters, tune fallbacks and font-display, limit downloads to the weights and subsets you use, use the fonts with Tailwind, and understand caching. Everything here was built and inspected with Astro 7.4.2.

Why Use the Fonts API

Web fonts affect performance in three ways: how many bytes are downloaded, how soon the download starts, and how much the layout shifts when the font arrives. The Fonts API addresses all three:

  • Self-hosting: font files from providers are downloaded at build time and served from /_astro/fonts/, so there's no extra connection to a third-party domain and no visitor data is sent to it.
  • Minimal downloads: by default only weight 400, normal and italic styles, the Latin subset and WOFF2 files are included. You opt in to anything else.
  • Optimised fallbacks: Astro generates a fallback @font-face with size-adjust and metric overrides, so the system font takes up nearly the same space as the web font.
  • Controlled preloading: you decide per font, and even per file, what gets a <link rel="preload">.
  • Type safety: cssVariable values are typed, so a typo in <Font cssVariable="..." /> is caught by astro check.

Using a Local Font File

If you have the font files yourself, for example a licensed typeface or a variable font downloaded from the designer, put them in src/:

src/
└── assets/
    └── fonts/
        └── InterVariable.woff2

Then register the font with the local provider:

// astro.config.mjs
import { defineConfig, fontProviders } from "astro/config";

export default defineConfig({
  fonts: [
    {
      provider: fontProviders.local(),
      name: "Inter",
      cssVariable: "--font-inter",
      fallbacks: ["sans-serif"],
      options: {
        variants: [
          {
            src: ["./src/assets/fonts/InterVariable.woff2"],
            weight: "100 900",
            style: "normal",
          },
        ],
      },
    },
  ],
});

Each object in variants becomes one @font-face rule. For a variable font, weight is a range string like "100 900". For static fonts, add one variant per file with a single weight such as 400 or 700. If you leave out weight and style, Astro tries to infer them from the file. The src paths are relative to the project root.

Using a Font Provider

For open-source fonts you don't need to download anything. Astro has built-in providers for Adobe, Bunny, Fontshare, Fontsource, Google, Google Icons and NPM. Here's Lora from Fontsource:

// astro.config.mjs
import { defineConfig, fontProviders } from "astro/config";

export default defineConfig({
  fonts: [
    {
      provider: fontProviders.fontsource(),
      name: "Lora",
      cssVariable: "--font-lora",
      weights: [400, 700],
      styles: ["normal"],
      fallbacks: ["serif"],
    },
  ],
});

The name must match how the provider identifies the font. During the build, Astro fetched the two requested files and logged:

[assets] Copying fonts (3 files)...

Three files: one for local Inter, two for Lora's weights. For a variable font from a provider, pass a range in weights, for example weights: ["300 700"].

Applying Fonts with the Font Component

Registering a font doesn't add anything to your pages yet. You include it where it's needed with the <Font /> component, usually in your base layout's head:

---
// src/layouts/BaseLayout.astro
import { Font } from "astro:assets";
import "../styles/global.css";

const { title } = Astro.props;
---
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width" />
    <title>{title}</title>
    <Font cssVariable="--font-inter" preload />
    <Font cssVariable="--font-lora" />
  </head>
  <body>
    <slot />
  </body>
</html>

Then use the CSS variables anywhere:

/* src/styles/global.css */
body {
  font-family: var(--font-inter);
}

h1,
h2 {
  font-family: var(--font-lora);
}

Because <Font /> is per page, you can include a font only in the layouts that use it. A display font used only on the homepage doesn't need to load on every blog post.

What Astro Generates

Here's the relevant part of the built <head> for the Inter configuration above:

<style>
@font-face{font-family:Inter-57eae95c9bd7fcb7;src:url("/_astro/fonts/cb13050e68e771d7.woff2") format("woff2");font-display:swap;font-weight:100 900;font-style:normal;}
@font-face{font-family:"Inter-57eae95c9bd7fcb7 fallback: Arial";src:local("Arial");font-display:swap;font-weight:100 900;font-style:normal;size-adjust:107.1194%;ascent-override:90.4365%;descent-override:22.518%;line-gap-override:0%;}
:root{--font-inter:Inter-57eae95c9bd7fcb7,"Inter-57eae95c9bd7fcb7 fallback: Arial",sans-serif;}
</style>
<link rel="preload" href="/_astro/fonts/cb13050e68e771d7.woff2" as="font" type="font/woff2" crossorigin>

A few things to notice:

  • The family name is hashed: you never type "Inter" in CSS. Always use the variable, which contains the full stack.
  • The fallback is metric-matched: because the last fallback is the generic sans-serif, Astro picked Arial and computed size-adjust and override values so it occupies almost the same space as Inter. This is the technique that reduces layout shift when the web font swaps in.
  • font-display: swap is the default: text appears immediately in the fallback font.
  • The preload link includes crossorigin: required for fonts, and easy to forget when writing it by hand.

For Lora, a serif, Astro generated fallbacks based on Times New Roman, including a separate one for the bold weight, and added a unicode-range for the Latin subset so the file is only downloaded when Latin characters are present.

Preload Only What You Need

Preloading tells the browser to fetch a file immediately, before it has discovered that the page needs it. That helps the font used for above-the-fold text, and hurts if you preload files the page doesn't use, because they compete with other critical resources.

The preload prop accepts true for every file of a font, or an array of filters with weight, style and subset:

<Font cssVariable="--font-inter" preload />
<Font cssVariable="--font-lora" preload={[{ weight: 700 }]} />

With this change, the build produced exactly two preload links: one for Inter's single variable file and one for Lora 700, used by the headings. Lora 400 was declared but not preloaded. For variable fonts, a file is preloaded if the requested weight falls within its range, so { weight: 400 } matches a 100 900 file.

A sensible rule: preload at most the body font and the heading font, and only the weights visible on first paint.

Tuning Fallbacks and font-display

Choose the Right Generic Family

The optimised fallback is generated from the last entry in fallbacks, if it's a generic family. The default is ["sans-serif"], which is wrong for serif or monospace fonts, so set it explicitly:

{
  provider: fontProviders.fontsource(),
  name: "JetBrains Mono",
  cssVariable: "--font-mono",
  fallbacks: ["monospace"],
}

You can list specific fonts before the generic one, such as ["Georgia", "serif"]. To turn off the metric adjustment, set optimizedFallbacks: false. To remove fallbacks entirely, use an empty array, though that's rarely a good idea.

Pick a font-display Strategy

The display option sets font-display on the generated rules. It defaults to "swap":

{
  provider: fontProviders.fontsource(),
  name: "Lora",
  cssVariable: "--font-lora",
  display: "optional",
}

With "optional", the built CSS contained font-display:optional. That value lets the browser skip the web font on slow connections and keep the fallback, eliminating late swaps entirely. It's a good fit for decorative fonts. For body text, "swap" combined with the optimised fallback is usually the best balance. The effect on layout shift is discussed in the post on Core Web Vitals in Astro.

Download Only What You Use

Every extra weight, style and subset is another file. The defaults are deliberately small:

OptionDefaultWhen to change it
weights[400]Add each weight your CSS actually uses, or a range for variable fonts
styles["normal", "italic"]Use ["normal"] if you never use italics
subsets["latin"]Add "latin-ext", "cyrillic" and so on for other languages
formats["woff2"]Rarely; WOFF2 is supported by every current browser
display"swap""optional" for decorative fonts

For fine-grained control, declare the same font more than once with the same name, cssVariable and provider. Astro merges the entries and downloads only the combinations you listed:

// astro.config.mjs
import { defineConfig, fontProviders } from "astro/config";

export default defineConfig({
  fonts: [
    {
      name: "Roboto",
      cssVariable: "--font-roboto",
      provider: fontProviders.google(),
      weights: [500, 600],
      styles: ["normal"],
    },
    {
      name: "Roboto",
      cssVariable: "--font-roboto",
      provider: fontProviders.google(),
      weights: [500],
      styles: ["italic"],
    },
  ],
});

This fetches normal 500 and 600, plus italic 500 only. The unicodeRange option is also available when part of a site uses a different script and you want the browser to download that font only on pages that contain those characters.

Using the Fonts with Tailwind CSS

With Tailwind 4, map the font's CSS variable to a theme variable using @theme inline:

/* src/styles/global.css */
@import "tailwindcss";

@theme inline {
  --font-sans: var(--font-inter);
  --font-serif: var(--font-lora);
}

inline matters: because these theme values reference other variables, it tells Tailwind to put the var(--font-inter) reference directly into the generated utilities, which is what the Astro docs recommend. Now font-sans and font-serif utilities use your fonts. The rest of the Tailwind setup is covered in the post on Tailwind CSS in Astro.

Caching and Build Output

In production builds, font files are copied to dist/_astro/fonts/ with hashed names, so they can be served with long-lived cache headers like the rest of your static assets. Downloads from providers are cached between builds in node_modules/.astro/fonts; in my sandbox that folder held the three .woff2 files plus a provider cache directory. In development, the cache lives in .astro/fonts.

If a font looks stale after you change its configuration, delete the relevant cache folder and rebuild.

Using Font Files in Code

Astro also exposes the downloaded fonts programmatically through fontData from astro:assets, keyed by cssVariable. Each entry lists the files and their URLs, which is useful when another tool needs the raw font, such as Satori when generating Open Graph images in an endpoint. The docs pair it with experimental_getFontFileURL() to turn a font path into a fetchable URL; note that this helper is still marked experimental. The post on Open Graph images in Astro shows this kind of endpoint in practice.

Migrating from Older Approaches

If your project predates the Fonts API, you probably have one of these:

  • A Google Fonts <link> tag: replace it with fontProviders.google() and a <Font /> component. The fonts are then self-hosted, which removes the third-party connection.
  • Fontsource packages imported in a layout, like import "@fontsource/inter";: replace them with fontProviders.fontsource() and uninstall the packages.
  • Hand-written @font-face rules in a global stylesheet: move the files into src/assets/fonts/ and use fontProviders.local().
  • experimental.fonts: true from Astro 5: remove the flag, since the API is stable in Astro 6 and later.

FAQ: Custom Fonts in Astro

No. It became stable in Astro 6, so you configure it with the top-level fonts option in astro.config.mjs without an experimental flag. Only the experimental_getFontFileURL helper keeps an experimental prefix.

No. Astro downloads the files at build time and serves them from your own site under _astro/fonts, so visitors' browsers never contact the provider.

Astro gives each font a hashed family name to avoid collisions. Use the CSS variable you configured, such as var(--font-inter), which contains the font and its fallbacks.

No. Preload only the files needed for text visible on first paint, usually the body font and one heading weight. The preload prop accepts an array of weight, style and subset filters for this.

For a local file, set the variant's weight to a range string such as 100 900. For providers, set weights to an array containing a range, such as 300 700 as a single string.

Put them in the src folder, for example src/assets/fonts, and reference them from the variant's src array with a path relative to the project root.


Conclusion

The Astro Fonts API turns font loading into configuration: declare each font once in astro.config.mjs, add <Font /> to the head of the layouts that use it, and reference the CSS variable in your styles. Behind that small surface, Astro self-hosts the files, keeps downloads to the weights and subsets you asked for, writes the @font-face rules and generates metric-adjusted fallbacks that reduce layout shift.

To make it efficient rather than just convenient, be deliberate about three settings: list only the weights and styles you use, set fallbacks to the right generic family, and preload only the files needed for the first screen. With those in place, custom fonts cost you a few small, cached WOFF2 files and almost no layout shift.

Tags :
Share :

Related Posts

What are Astro Actions and how do they work?

What are Astro Actions and how do they work?

Astro Actions are type-safe backend functions that you define once on the server and then call from the browser as if they were ordinary async fu

Dive Deeper
What are Astro adapters and which one should you choose?

What are Astro adapters and which one should you choose?

An Astro adapter is an integration that builds the server part of your project for a specific runtime, such as Node.js, Vercel Functions, Netlify

Dive Deeper
How to build API endpoints in Astro?

How to build API endpoints in Astro?

To build an API endpoint in Astro, create a .js or .ts file in src/pages/ and export a function named after an HTTP method, such as GET or

Dive Deeper