npm.io
3.14.0 • Published 3h ago

vite-plugin-webfont-dl

Licence
MIT
Version
3.14.0
Deps
3
Size
71 kB
Vulns
0
Weekly
0
Stars
383

vite-plugin-webfont-dl

NPM version NPM downloads all-time NPM downloads last month License

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 @import statements
  • 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.

  1. 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">
  2. Add webfontDownload to your Vite plugins without any configuration:

    // vite.config.js
    import webfontDownload from 'vite-plugin-webfont-dl';
    
    export default {
      plugins: [
        webfontDownload(),
      ],
    };
  3. 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).

  1. 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">
  2. 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, or viteWebfontDownload.

Examples: the examples directory has minimal, runnable projects for vanilla, Vue, React, Svelte, and React Router.

Terminal output showing downloaded webfonts

Supported webfont providers

Provider Zero config
(HTML <link>)
Simple config
(CSS URL)
CSS @import
detection
Google Fonts
Bunny Fonts
Fontshare
Fira Code & Hack (cdn.jsdelivr.net) —
Inter (rsms.me) —
Any CSS with @font-face definitions — —

CSS @import detection: in addition to HTML tags and plugin config, the plugin also scans your bundled CSS for @import statements pointing at the providers marked above and self-hosts those fonts as well (example). The same goes for @font-face rules in your CSS whose font files are hosted on fonts.gstatic.com or fonts.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:

  • fontDefinitionsInCss applies to production builds. On the development server the @import is 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 as webfonts.css when 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 onload handler, which strict CSP environments (e.g. Chrome extensions) prohibit. In those environments, keep the default injectAsStyleTag: true, or set async: false for standard blocking CSS loading.

Note: embedFonts can 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:

  1. The first line hints the browser to begin the connection handshake (DNS, TCP, TLS) with fonts.googleapis.com in the background. [preconnect]
  2. The second line is another preconnect hint, for fonts.gstatic.com. [preconnect]
  3. The third line instructs the browser to load and use a CSS stylesheet from fonts.googleapis.com (with font-display: swap). [stylesheet]
  4. The browser downloads and parses the CSS: a set of @font-face definitions with font URLs pointing at fonts.gstatic.com.
  5. The browser downloads all relevant fonts from fonts.gstatic.com.
  6. 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:

  1. Fonts load directly from the embedded CSS (<style> tag). Or:
  2. The first line instructs the browser to prefetch the CSS file for later use as a stylesheet. [preload]
  3. The second line loads the CSS file as a print stylesheet (non-render-blocking), then promotes it to an all media stylesheet once loaded, by removing the media attribute. [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

Performance comparison of standard Google Fonts vs vite-plugin-webfont-dl

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.

Keywords