---
title: Self-host web fonts on Shopify CDN
description: >-
  Upload web fonts to your theme's `assets/` directory and serve them using the
  Shopify CDN to eliminate external DNS connections and enable Early Hints
  delivery.
source_url:
  html: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/self-host-web-fonts
  md: >-
    https://shopify.dev/docs/storefronts/themes/best-practices/performance/self-host-web-fonts.md
api_name: liquid
---

# Self-host web fonts on Shopify CDN

When a typeface isn't available in the Shopify font library, self-host it on the Shopify CDN instead of loading it from an external service such as Google Fonts. This eliminates connection overhead and multiple requests.

***

## Why

Self-hosting fonts eliminates external domain connections, uses automatic Shopify CDN delivery, provides automatic version cache busting through [`asset_url`](https://shopify.dev/docs/api/liquid/filters/asset_url), enables faster font discovery and download, works with [Early Hints](https://performance.shopify.com/blogs/blog/early-learnings-for-early-hints-at-shopify) through Liquid filters, and removes one external dependency.

* **Self-hosted fonts**: eliminate external connections. Fastest approach.
* **Optimized Google Fonts**: reduces connection overhead with `preconnect` and async loading.
* **Unoptimized Google Fonts**: multiple separate connections. Slowest approach.

### When to self-host

If the typeface you need is in the [Shopify font library](https://shopify.dev/docs/storefronts/themes/architecture/settings/fonts#shopify-font-library), then reach for a [`font_picker` setting](https://shopify.dev/docs/storefronts/themes/architecture/settings/input-settings#font_picker) with the [`font_face`](https://shopify.dev/docs/api/liquid/filters/font_face) and [`font_url`](https://shopify.dev/docs/api/liquid/filters/font_url) filters first. Those filters serve the font from the Shopify CDN, normally under `/cdn/fonts/` on the domain that served the page, so they avoid the external connection for the same reason self-hosting does. They also let merchants change the font in the theme editor without a code change, and they cover the bold and italic variants through [`font_modify`](https://shopify.dev/docs/api/liquid/filters/font_modify). Shopify's own themes, including Dawn, use this approach.

Self-host when the font filters can't do the job:

* The typeface isn't in the Shopify font library, such as a licensed brand font.
* You need a subset, variable axis, or Unicode range that the library's files don't cover.
* The typeface is fixed by brand guidelines, so it shouldn't be a merchant-editable setting.

If none of those apply, then the font filters are the lower-maintenance option and reach the same result.

***

## How

### Step 1: Download font files

Use [Google Webfonts Helper](https://gwfh.mranftl.com/fonts) to download Google Fonts. This tool provides WOFF2 and WOFF files for modern browsers, and CSS snippets ready to use. You need WOFF2 as the primary format for best compression, and WOFF as the fallback for older browsers. Skip EOT, TTF, and SVG, because modern browsers don't need them.

### Step 2: Add fonts to theme assets

Place downloaded font files in the `/assets` directory:

```text
theme/
  assets/
    montserrat-v31-latin-regular.woff2
    montserrat-v31-latin-regular.woff
    montserrat-v31-latin-700.woff2
    montserrat-v31-latin-700.woff
```

### Step 3: Define `@font-face` with `asset_url`

Create inline styles in your layout file, such as `theme.liquid`, or in a `.css.liquid` file:

## layout/theme.liquid

```liquid
<style>
  /* montserrat-regular - latin */
  @font-face {
    font-display: swap;
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 400;
    src: url('{{ "montserrat-v31-latin-regular.woff2" | asset_url }}') format('woff2'),
         url('{{ "montserrat-v31-latin-regular.woff" | asset_url }}') format('woff');
  }


  /* montserrat-700 - latin */
  @font-face {
    font-display: swap;
    font-family: 'Montserrat';
    font-style: normal;
    font-weight: 700;
    src: url('{{ "montserrat-v31-latin-700.woff2" | asset_url }}') format('woff2'),
         url('{{ "montserrat-v31-latin-700.woff" | asset_url }}') format('woff');
  }
</style>
```

Defining fonts inline in the `<head>` ensures that they're discovered immediately, which is faster than loading an external CSS file first. The `asset_url` filter provides automatic cache busting through version numbers.

### Step 4: Use the font

Reference the font family in your CSS:

```css
body {
  font-family: "Montserrat", system-ui, -apple-system, sans-serif;
}
```

Always provide fallback fonts in case the web font fails to load.

### Step 5: Preload critical fonts

For fonts used for content visible in the initial viewport, preload them for faster rendering:

```liquid
{%- comment -%} Preload primary font {%- endcomment -%}
{{ 'montserrat-v31-latin-regular.woff2' | asset_url | preload_tag: as: 'font', type: 'font/woff2' }}


<style>
  @font-face {
    font-display: swap;
    font-family: 'Montserrat';
    src: url('{{ "montserrat-v31-latin-regular.woff2" | asset_url }}') format('woff2');
  }
</style>
```

**Note:**

Font preloads must be CORS-enabled, even for same-origin fonts. Don't pass `crossorigin` to `preload_tag`: it emits `crossorigin="anonymous"` on its own when `as` is `font`.

### Alternative: `.css.liquid` file

If you prefer to keep `@font-face` declarations in a CSS file, then create `assets/fonts.css.liquid`:

## assets/fonts.css.liquid

```liquid
/* montserrat-regular - latin */
@font-face {
  font-display: swap;
  font-family: 'Montserrat';
  font-style: normal;
  font-weight: 400;
  src: url('{{ "montserrat-v31-latin-regular.woff2" | asset_url }}') format('woff2'),
       url('{{ "montserrat-v31-latin-regular.woff" | asset_url }}') format('woff');
}
```

Include in `theme.liquid`:

```liquid
{{ 'fonts.css' | asset_url | stylesheet_tag }}
```

The source file must have a `.css.liquid` extension for Shopify to process the Liquid code inside it. Reference it as `fonts.css`, without the `.liquid` extension: Shopify serves the compiled asset under that name.

### Self-hosting best practices

Always use `font-display: swap` to avoid invisible text while fonts load:

```css
@font-face {
  font-display: swap; /* Show the fallback font immediately */
  font-family: "Montserrat";
  src: url("...") format("woff2");
}
```

This prevents FOIT (Flash of Invisible Text) and lets customers see content immediately with a fallback font.

Limit the number of fonts. Each font family and weight requires a separate download. 1 to 2 font families is good, 2 to 4 weights per family is reasonable, and more than 6 total font files impacts performance.

Don't forget fallback fonts. See [Reduce CLS from font swapping](https://shopify.dev/docs/storefronts/themes/best-practices/performance/reduce-cls-font-swapping) for information on reducing CLS from font swapping by customizing fallback fonts to match web font dimensions.

***

## Examples

### When self-hosting isn't possible

Some font services don't allow self-hosting because of licensing restrictions. If you can't self-host, then add `preconnect` hints to warm up the connection early and minimize latency.

#### Adobe Typekit

Typekit fonts can't be self-hosted. Add `preconnect` for both Typekit domains:

```html
<link rel="preconnect" href="https://p.typekit.net" />
<link rel="preconnect" href="https://use.typekit.net" crossorigin />
```

#### Google Fonts

Sometimes you must use the Google Fonts API directly because of dynamic font pickers or client requirements. If you can't self-host, then minimize the performance impact:

1. Make the stylesheet async:

```liquid
{%- comment -%} Load the Google Fonts CSS asynchronously {%- endcomment -%}
<link
  rel="stylesheet"
  href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap"
  media="print"
  onload="this.media='all'"
/>
<noscript>
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap">
</noscript>
```

1. Add `preconnect`:

```html
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
```

1. Limit font weights.

Load only the weights you actually use. Good: `?family=Roboto:wght@400;700`. Anti-pattern: `?family=Roboto:wght@100;200;300;400;500;600;700;800;900`.

***

## Testing

* **[Chrome DevTools Network panel](https://developer.chrome.com/docs/devtools/network)**: count connections to font domains. The count should be zero for self-hosted fonts.
* **Network tab**: verify that fonts load from `cdn.shopify.com` or your store domain.
* **Compare timings**: self-hosting eliminates external domain connections. Check the waterfall for fewer DNS and connection bars.

***

## References

* [`asset_url`](https://shopify.dev/docs/api/liquid/filters/asset_url) filter
* [`preload_tag`](https://shopify.dev/docs/api/liquid/filters/preload_tag) filter
* [`font_face`](https://shopify.dev/docs/api/liquid/filters/font_face) filter
* [`font_picker` setting](https://shopify.dev/docs/storefronts/themes/architecture/settings/input-settings#font_picker)
* [Google Webfonts Helper](https://gwfh.mranftl.com/fonts)
* [Shopify fonts architecture](https://shopify.dev/docs/storefronts/themes/architecture/settings/fonts)
* [MDN: font-display](https://developer.mozilla.org/en-US/docs/Web/CSS/@font-face/font-display)
* [Reduce CLS from font swapping](https://shopify.dev/docs/storefronts/themes/best-practices/performance/reduce-cls-font-swapping)
* [Use system fonts](https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-system-fonts)
* [Serve assets from Shopify CDN](https://shopify.dev/docs/storefronts/themes/best-practices/performance/use-shopify-cdn)

***
