Fonts
Purpose and Scope
Astro's Fonts API gives projects a single configuration model for using hosted fonts, local font files, package-managed fonts, and icon fonts without asking every page author to hand-write matching @font-face rules. A font family in this system is not just a CSS name. It combines a project-facing family name, a CSS variable, one provider, and optional constraints such as weights, styles, subsets, formats, fallbacks, display behavior, variation settings, and provider-specific options. The official docs describe this as a unified way to access fonts, where each family uses a provider that either downloads files from a remote service or loads local files from disk.
Sources: packages/astro/src/assets/fonts/config.ts, packages/astro/src/assets/fonts/types.ts
The implementation is split between validation, provider adapters, type contracts, and a Vite plugin. The validation layer accepts only the configuration shape Astro knows how to collect and render. The provider layer normalizes external font services and local sources behind the same FontProvider contract. The type layer defines what a provider receives and returns. The Vite plugin connects all of that to Astro's dev and build pipelines, collecting component usage, resolving font faces, generating asset URLs, optimizing fallback metrics, and serving or emitting font files as static assets.
Sources: packages/astro/src/assets/fonts/config.ts, packages/astro/src/assets/fonts/providers/index.ts, packages/astro/src/assets/fonts/types.ts, packages/astro/src/assets/fonts/vite-plugin-fonts.ts
Relevant Source Files
packages/astro/src/assets/fonts/config.tsdefines the Zod schemas that validate font provider objects and configured font families, including weights, styles, formats, fallbacks, display, CSS variables, and provider options.packages/astro/src/assets/fonts/providers/index.tsexposes Astro's built-in provider factories overunifont, including Adobe, Bunny, Fontshare, Fontsource, Google, Google Icons, Local, and NPM-oriented provider support.packages/astro/src/assets/fonts/types.tsdefines the public TypeScript contract for providers, provider initialization, family properties, typed provider options, and font-family configuration behavior.packages/astro/src/assets/fonts/vite-plugin-fonts.tswires fonts into Vite and Astro, creating the runtime and build behavior that collects font data, resolves assets, caches downloads, computes output URLs, renders CSS, optimizes fallbacks, and serves font files.
Core Primitives
The central authoring primitive is a font family configuration. FontFamilySchema requires a name, a cssVariable, and a provider, then allows optional controls for weights, styles, subsets, formats, fallbacks, optimizedFallbacks, display, stretch, featureSettings, variationSettings, unicodeRange, and options. These names are important because they describe what Astro can understand before it starts collecting or resolving anything. The schema is strict, so the configuration contract is intentionally narrow: values outside the known font-family shape are rejected rather than silently carried through the pipeline.
Sources: packages/astro/src/assets/fonts/config.ts
A provider is the second primitive. FontProvider has a unique name, optional serializable config, optional init(context), required resolveFont(options), and optional listFonts(). The initialization context gives providers a project root URL and a storage interface with getItem() and setItem() methods, which is useful for caching remote metadata or downloaded results. resolveFont() returns font face data in the form expected by unifont, or undefined when the provider cannot resolve the requested family. This gives Astro a stable boundary between project configuration and external font infrastructure.
Sources: packages/astro/src/assets/fonts/types.ts
Family-level properties are the third primitive because they control how the resolved font face becomes usable CSS. The display property defaults to the same behavior documented in the type comment, swap , and maps to CSS font-display. stretch, featureSettings, variationSettings, and unicodeRange map to @font-face descriptors that affect browser selection and downloading. unicodeRange is particularly practical for localized sites because it lets the browser avoid downloading a font unless characters in a declared range appear on the page. Subsets are a separate concept: they describe characters preloaded for a single font family rather than the browser's conditional range matching.
Sources: packages/astro/src/assets/fonts/types.ts
Built-In Providers
Astro's built-in providers are implemented as small adapters around unifont providers. The official docs show importing fontProviders from astro/config and using entries such as fontProviders.adobe({ id: "your-id" }), fontProviders.bunny(), fontProviders.fontshare(), fontProviders.fontsource(), fontProviders.google(), fontProviders.googleicons(), fontProviders.local(), and fontProviders.npm(). In the source, the remote provider wrappers create a unifont provider, retain an initializedProvider variable, initialize it during init(context), and delegate resolveFont() and listFonts() to the initialized implementation.
Sources: packages/astro/src/assets/fonts/providers/index.ts
This adapter pattern means Astro's font system does not need service-specific logic everywhere in the build pipeline. Adobe needs an ID, Google exposes family-specific options, and local or NPM-based providers have different ways to find font files, but the rest of Astro receives the same provider object shape. Google and Google Icons are typed with provider-specific family options, while providers without family options can omit the options object. The type helper in types.ts preserves this distinction so configuration authors get provider-specific typing instead of an unstructured catch-all for every family.
Sources: packages/astro/src/assets/fonts/providers/index.ts, packages/astro/src/assets/fonts/types.ts
Google has additional documented family options for experimental glyph selection and variable-axis configuration. Those options are passed through the family options field rather than becoming top-level Astro font-family properties. That distinction matters when reading or writing configuration: top-level fields such as weights, styles, display, and fallbacks are Astro's normalized cross-provider vocabulary, while options belongs to the chosen provider. The schema intentionally models options as a record so provider adapters can evolve without requiring a new Astro-level field for every service-specific capability.
Sources: packages/astro/src/assets/fonts/config.ts, packages/astro/src/assets/fonts/types.ts
Configuration Reference
The configuration schema accepts the following core fields for a font family: name: string, cssVariable: string, and provider: FontProvider. It also accepts weights, styles, subsets, formats, fallbacks, optimizedFallbacks, display, stretch, featureSettings, variationSettings, unicodeRange, and options. weights may be strings or numbers. styles are limited to normal, italic, and oblique. display is limited to auto, block, swap, fallback, and optional. formats are constrained by Astro's font type constants rather than accepting arbitrary strings.
Sources: packages/astro/src/assets/fonts/config.ts
// astro.config.mjs
import { defineConfig, fontProviders } from 'astro/config';
export default defineConfig({
experimental: {
fonts: [{
name: 'Inter',
cssVariable: '--font-inter',
provider: fontProviders.google(),
weights: [400, 700],
styles: ['normal'],
subsets: ['latin'],
display: 'swap',
fallbacks: ['system-ui', 'sans-serif'],
optimizedFallbacks: true,
options: {
experimental: {
glyphs: ['a']
}
}
}]
}
});Custom providers should implement the same FontProvider interface used by built-ins. A provider must expose a stable name and a resolveFont() function. It may expose serializable config, init(context), and listFonts(). The schema deliberately validates providers by checking the shape and then returning the original object, rather than remapping through a plain Zod object. The source comment explains why: direct object remapping would prevent class instances from being used as provider objects. That is a useful implementation detail for integration authors who prefer class-based provider implementations.
Sources: packages/astro/src/assets/fonts/config.ts, packages/astro/src/assets/fonts/types.ts
import type { FontProvider } from 'astro';
const customProvider: FontProvider = {
name: 'my-provider',
async init({ storage, root }) {
await storage.setItem('root', root.href);
},
async resolveFont({ familyName }) {
return { fonts: [] };
},
async listFonts() {
return ['Example Sans'];
}
};Build-Time Collection and Runtime Behavior
The font Vite plugin is where configuration becomes files, URLs, and CSS. It computes a font asset directory from settings.config.build.assets, appends Astro's font asset directory, and combines that with settings.config.base to get the base URL for generated font assets. It also keeps shared maps for font files, component data by CSS variable, and font data by CSS variable, then resets them around build lifecycle boundaries to avoid retaining memory longer than needed. This makes fonts a build concern, but one that still participates in development server behavior.
Sources: packages/astro/src/assets/fonts/vite-plugin-fonts.ts
Several imported core modules describe the plugin's phases. collectComponentData and collectFontData gather the usage and configured family data that Astro needs before emitting CSS. resolveFamily, filterAndTransformFontFaces, collectFontAssetsFromFaces, computeFontFamiliesAssets, and getOrCreateFontFamilyAssets describe the resolution path from a family declaration to concrete font-face data and asset records. optimizeFallbacks connects the optimizedFallbacks option to font metrics work, while CapsizeFontMetricsResolver and RealSystemFallbacksProvider show that fallback optimization uses metrics and system fallback knowledge rather than only string substitution.
Sources: packages/astro/src/assets/fonts/vite-plugin-fonts.ts
The same plugin imports separate infrastructure for development and production. DevFontFileIdGenerator and DevUrlResolver support dev-server identifiers and URLs, while BuildFontFileIdGenerator and BuildUrlResolver support final build output. CachedFontFetcher, UnstorageFsStorage, and XxhashHasher point to a cached and content-addressed workflow for remote or generated font assets. FsFontFileContentResolver, NodeFontTypeExtractor, and MinifiableCssRenderer show the pipeline also needs to read font bytes, determine font types, and render CSS that can be minified for output.
Sources: packages/astro/src/assets/fonts/vite-plugin-fonts.ts
Runtime support appears through virtual module IDs and middleware. The plugin imports resolved and public virtual module identifiers for font runtime modules and a runtime font-file URL resolver, giving Astro code a way to reference generated font behavior without hard-coding physical files in user projects. It also imports fontFileMiddleware and resToMinimalResponse, which indicates that development or runtime requests for font files can be served through Astro's server path before the final build emits assets. The implementation is therefore not just a config validator; it is an asset pipeline with request-time behavior in dev-like environments.
Sources: packages/astro/src/assets/fonts/vite-plugin-fonts.ts
System-to-Code Mapping
| Concern | Source-level contract | Why it matters |
|---|---|---|
| Family validation | FontFamilySchema | Defines the supported Astro font configuration shape before collection begins. |
| Provider validation | FontProviderSchema | Accepts function-based or class-instance providers that match the required provider shape. |
| Provider initialization | FontProvider.init(context) | Gives providers storage and project-root access before resolving families. |
| Font resolution | FontProvider.resolveFont(options) | Returns normalized font face data for the rest of Astro's pipeline. |
| Built-in services | adobe(), bunny(), fontshare(), fontsource(), google(), googleicons() | Wraps service-specific unifont providers behind Astro's common interface. |
| Build and dev pipeline | fontsPlugin({ settings, sync, logger }) | Connects font configuration to Vite, asset URLs, caching, CSS rendering, middleware, and generated virtual modules. |
Next Steps
Use the Fonts API when you want font loading to be part of Astro's configuration and build pipeline instead of scattered CSS. Start by choosing a provider from fontProviders, give each family a stable cssVariable, and keep cross-provider controls such as display, fallbacks, formats, and optimizedFallbacks at the family level. Put provider-specific behavior, such as Google glyph or variable-axis options, inside options. If you need a custom source, implement FontProvider directly and test that resolveFont() returns normalized face data before relying on fallback optimization or build asset emission.
Sources: packages/astro/src/assets/fonts/config.ts, packages/astro/src/assets/fonts/providers/index.ts, packages/astro/src/assets/fonts/types.ts, packages/astro/src/assets/fonts/vite-plugin-fonts.ts