vite-plugin-webfont-dl
Webfont Download — a Vite plugin that downloads webfonts at build time and self-hosts them, eliminating render-blocking requests to third-party font providers.
The plugin collects webfont links, imports, and definitions from your project, downloads the CSS and font files, adds the fonts to your bundle (or serves them through the dev server), and injects the font definitions using a non-render-blocking method.
Features
- Zero config — automatically detects webfont
<link>tags, plugin config URLs, and CSS@importstatements - Self-hosted fonts — font files are bundled with your app; no third-party requests at runtime
- Non-render-blocking — fonts are injected as an inline
<style>tag or an asynchronously loaded stylesheet - Privacy-first — visitors never connect to font CDNs, so no user data is exposed to third parties
- Persistent cache — downloaded CSS and font files are cached locally, enabling offline development
- Broad compatibility — works with Vite 2–8, including the Rolldown-based Vite 8 (see framework notes)
Installation
# npm
npm i -D vite-plugin-webfont-dl
# pnpm
pnpm add -D vite-plugin-webfont-dl
# yarn
yarn add -D vite-plugin-webfont-dl
# bun
bun add -d vite-plugin-webfont-dl
Usage
There are two alternative ways to use the plugin — choose whichever fits your project, you don't need both:
- Zero config — keep your webfont provider's original
<link>snippet in your HTML; the plugin detects and replaces it automatically. - Simple config — no
<link>tags in your HTML; pass the webfont CSS URL(s) directly to the plugin instead.
Both methods lead to the same result: self-hosted, non-render-blocking webfonts — see That's all! below.
Zero config
Extracts, downloads, and injects fonts from the original code snippet of your webfont provider.
Select your font families at your webfont provider (e.g., Google Fonts) and copy the code from the "Use on the web" block into your
<head>:<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300;400&family=Roboto:wght@100&display=swap" rel="stylesheet">Add
webfontDownloadto your Vite plugins without any configuration:// vite.config.js import webfontDownload from 'vite-plugin-webfont-dl'; export default { plugins: [ webfontDownload(), ], };The original webfont tags are replaced with self-hosted font definitions in
dist/index.html:<style>@font-face{font-family:...;src:url(/assets/foo-xxxxxxxx.woff2) format('woff2'),url(/assets/bar-yyyyyyyy.woff) format('woff')}...</style>
Simple config
Extracts, downloads, and injects fonts from the configured webfont CSS URL(s).
Select your font families at your webfont provider and copy the CSS URL(s) from the "Use on the web" code block:
<link href="[CSS URL]" rel="stylesheet">Pass the CSS URL(s) to the plugin:
// vite.config.js import webfontDownload from 'vite-plugin-webfont-dl'; export default { plugins: [ webfontDownload([ 'https://fonts.googleapis.com/css2?family=Press+Start+2P&display=swap', 'https://fonts.googleapis.com/css2?family=Fira+Code&display=swap', ]), ], };
That's all!
Whichever method you chose, the result is the same: the webfonts are downloaded, self-hosted, and injected — ready to use on both the local development server and in production builds:
h1 {
font-family: 'Press Start 2P', cursive;
}
h2 {
font-family: 'Fira Code', monospace;
}
Import alias: the plugin can be imported under any of these names:
webfontDownload,webfontDl,viteWebfontDl,ViteWebfontDownload, orviteWebfontDownload.
Examples: the
examplesdirectory has minimal, runnable projects for vanilla, Vue, React, Svelte, and React Router.

Supported webfont providers
| Provider | Zero config (HTML <link>) |
Simple config (CSS URL) |
CSS @importdetection |
|---|---|---|---|
| Google Fonts | |||
| Bunny Fonts | |||
| Fontshare | |||
Fira Code & Hack (cdn.jsdelivr.net) |
— | ||
Inter (rsms.me) |
— | ||
Any CSS with @font-face definitions |
— | — |
CSS
@importdetection: in addition to HTML tags and plugin config, the plugin also scans your bundled CSS for@importstatements pointing at the providers marked above and self-hosts those fonts as well (example). The same goes for@font-facerules in your CSS whose font files are hosted onfonts.gstatic.comorfonts.bunny.net.
Framework notes
The plugin works out of the box with any setup where Vite processes your HTML entry — plain Vite apps, Vue, React, Svelte, and backend integrations such as Laravel.
Laravel
To use with the Laravel Vite Plugin, add this line to your Blade file:
@vite('webfonts.css')
React Router, SvelteKit, and other frameworks that render the HTML themselves
Frameworks such as React Router (framework mode) or SvelteKit render the HTML outside of Vite, so the plugin has no HTML file to inject the fonts into. For these, import the webfonts in your CSS and enable the fontDefinitionsInCss option:
/* app/app.css */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap');
body {
font-family: Inter, sans-serif;
}
// vite.config.js
import webfontDownload from 'vite-plugin-webfont-dl';
export default {
plugins: [
// ...your framework's plugin
webfontDownload([], {
fontDefinitionsInCss: true,
}),
],
};
What the option changes — where the self-hosted font definitions of your CSS end up:
fontDefinitionsInCss: false (default) |
fontDefinitionsInCss: true |
|
|---|---|---|
| Your CSS file | the @import is removed |
the @import is replaced with the self-hosted @font-face definitions |
| Font definitions | moved into the HTML (<style> tag or webfonts.css) |
stay in your CSS file |
| Needs an HTML file built by Vite | yes | no |
/* app.css in the build output, with `fontDefinitionsInCss: true` (formatted for readability) */
body{font-family:Inter,sans-serif}
@font-face{font-family:Inter;font-weight:400;src:url(/assets/inter-xxxxxxxx.woff2) format('woff2')}
...
Your framework already links that CSS file, so the fonts just work. See the complete React Router example. Good to know:
fontDefinitionsInCssapplies to production builds. On the development server the@importis left untouched, so the fonts load from the webfont provider while developing.- It only affects font definitions found in your CSS (
@import,@font-face). Fonts from HTML<link>tags and from URLs passed to the plugin are still injected into the HTML, or saved aswebfonts.csswhen there is no HTML file. - Tested with React Router 8 and SvelteKit 3.
Astro
Astro renders HTML outside of Vite's pipeline, so the Vite hook this plugin relies on (transformIndexHtml) is never called and the fonts are never injected into your pages in Astro projects (withastro/astro#3310). The fontDefinitionsInCss option has no effect in Astro either.
Use Astro's built-in Fonts API instead — it provides the same benefits natively (downloaded and self-hosted fonts, preloading, and optimized fallbacks):
// astro.config.mjs
import { defineConfig, fontProviders } from 'astro/config';
export default defineConfig({
fonts: [
{
provider: fontProviders.google(),
name: 'Fira Code',
cssVariable: '--font-fira-code',
},
],
});
Options
| Option | Type | Default | Description |
|---|---|---|---|
injectAsStyleTag |
boolean |
true |
Inject the webfont CSS inline as a <style> tag. Set to false to emit an external .css file instead. |
async |
boolean |
true |
Load the external stylesheet asynchronously (non-render-blocking). Only applies when injectAsStyleTag is false. |
minifyCss |
boolean |
value of build.minify |
Minify the generated CSS during build. |
embedFonts |
boolean |
false |
Embed fonts into the CSS as base64 data URIs instead of emitting separate font files. |
assetsSubfolder |
string |
'' |
Place downloaded font files in a subfolder of the assets directory. |
cache |
boolean |
true |
Persistently cache downloaded CSS and font files (respects Vite's cacheDir). Set to false to disable caching and delete an existing cache. |
subsetsAllowed |
string[] |
[] |
Only download the listed subsets (e.g. ['latin', 'latin-ext']). An empty array allows all subsets. |
fontDefinitionsInCss |
boolean |
false |
Keep the font definitions found in your CSS (@import, @font-face) in that same CSS file, self-hosted, instead of moving them into the HTML. For frameworks that render the HTML themselves. |
proxy |
false | AxiosProxyConfig |
false |
Proxy configuration for network requests. |
throwError |
boolean |
false |
Stop the build when a font fails to download or process. When false, errors are logged and the build continues: a file the webfont provider refuses (e.g. 404) stays on the provider while everything else is self-hosted; on a network error nothing is changed. |
Content Security Policy: the async loading technique uses an inline
onloadhandler, which strict CSP environments (e.g. Chrome extensions) prohibit. In those environments, keep the defaultinjectAsStyleTag: true, or setasync: falsefor standard blocking CSS loading.
Note:
embedFontscan increase the output size if the CSS references the same font file multiple times (example).
Example configuration
// vite.config.js
import webfontDownload from 'vite-plugin-webfont-dl';
export default {
plugins: [
webfontDownload([], {
injectAsStyleTag: true,
minifyCss: true,
embedFonts: false,
async: true,
cache: true,
proxy: false,
assetsSubfolder: 'fonts',
subsetsAllowed: ['latin', 'latin-ext'],
fontDefinitionsInCss: false,
throwError: false,
}),
],
};
With webfont CSS URLs:
webfontDownload([
'https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;500;600;700&display=swap',
], {
injectAsStyleTag: true,
cache: true,
})
Why self-host webfonts?
Adding third-party webfonts (Google Fonts, Bunny Fonts, or Fontshare) the standard way can significantly slow down page load. Lighthouse and PageSpeed Insights flag the external stylesheets as render-blocking resources: the page can't fully render until the webfont CSS has been fetched from the remote server.
This plugin downloads the fonts at build time and injects them into your project as an internal or external stylesheet, turning third-party webfonts into self-hosted ones. Eliminating the render-blocking requests improves page performance, user experience, and SEO — and since no third-party server is involved, your visitors' privacy is protected as well.
Further reading: Page Speed Checklist — Fix & Eliminate Render Blocking Resources
How it works
Standard Google Fonts
Google Fonts generates a code snippet that you inject into your website's <head> (example):
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Fira+Code&display=swap" rel="stylesheet">
What happens on the client side:
- The first line hints the browser to begin the connection handshake (DNS, TCP, TLS) with
fonts.googleapis.comin the background. [preconnect] - The second line is another preconnect hint, for
fonts.gstatic.com. [preconnect] - The third line instructs the browser to load and use a CSS stylesheet from
fonts.googleapis.com(withfont-display: swap). [stylesheet] - The browser downloads and parses the CSS: a set of
@font-facedefinitions with font URLs pointing atfonts.gstatic.com. - The browser downloads all relevant fonts from
fonts.gstatic.com. - Once the fonts are downloaded, the browser swaps the fallback fonts for the webfonts.
With vite-plugin-webfont-dl
The plugin does most of this work at build time, leaving minimal work for the browser. It:
- Collects the webfont CSS URLs (from plugin config,
index.html, and generated CSS) - Downloads the webfont CSS file(s)
- Extracts the font URLs
- Downloads the font files and adds them to the bundle
- Generates embedded CSS (
<style>tag) or an external webfont CSS file - Injects the result into your website's
<head>using a non-render-blocking method (example):
<style>
@font-face {
font-family: 'Fira Code';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url(/assets/uU9eCBsR6Z2vfE9aq3bL0fxyUs4tcw4W_GNsJV37Nv7g.9c348768.woff2) format('woff2');
unicode-range: U+0460-052F, U+1C80-1C88, U+20B4, U+2DE0-2DFF, U+A640-A69F, U+FE2E-FE2F;
}
...
</style>
Or, when using the dev server or the injectAsStyleTag: false option:
<link rel="preload" as="style" href="/assets/webfonts.b904bd45.css">
<link rel="stylesheet" media="print" onload="this.onload=null;this.removeAttribute('media');" href="/assets/webfonts.b904bd45.css">
What happens on the client side:
- Fonts load directly from the embedded CSS (
<style>tag). Or: - The first line instructs the browser to prefetch the CSS file for later use as a stylesheet. [
preload] - The second line loads the CSS file as a
printstylesheet (non-render-blocking), then promotes it to anallmedia stylesheet once loaded, by removing themediaattribute. [stylesheet]
Benchmark
Comparison using a starter Vite project:
| Standard Google Fonts | vite-plugin-webfont-dl |
|---|---|
| webfont.feat.agency | webfont-dl.feat.agency |
| PageSpeed Insights | PageSpeed Insights |

Contributing
Bug reports, feature requests, and pull requests are welcome on the issue tracker.
npm install # install dependencies
npm start # build in watch mode
npm test # run the test suite
npm run lint # type-check and lint
The examples double as integration tests: build the plugin (npm run build), then build an example and verify its output with node examples/check.mjs <output directory>.
License
MIT License 2022–present feat.