Skip to main content

Eliminate font loading delays with system fonts

Use system fonts to avoid downloading web font files before text can render.


Every web font file your theme loads creates a render dependency. Before styled text appears, the browser must:

  1. Parse the HTML and discover the @font-face declaration.
  2. Request the font file from the CDN.
  3. Wait for the file to download.
  4. Apply the font to text in the layout.

On a fast connection this takes tens of milliseconds. On a slow connection or mobile device, it can take several hundred milliseconds. During this time, browsers either show invisible text, known as FOIT (Flash of Invisible Text), or fall back to a system font before swapping after the web font arrives, known as FOUT (Flash of Unstyled Text). The swap itself can contribute to CLS if the web font's metrics differ from the fallback.

System fonts are already installed on the device. Using them means the browser can render text immediately: no network request, no swap, no layout shift from font loading.

Anchor to When to choose system fontsWhen to choose system fonts

  • Performance is the top priority.
  • The brand doesn't have a distinctive typographic identity that requires a custom font.
  • The theme targets mobile users on constrained connections.
  • You want to eliminate all font-related CLS.

Anchor to When web fonts are appropriateWhen web fonts are appropriate

  • The brand requires a specific typeface for identity or design consistency.
  • The performance cost has been measured and is acceptable for the target audience.
  • You're already applying self-hosting and fallback font tuning. See Self-host web fonts and Reduce CLS from font swapping.

Anchor to Use the system font stack in CSSUse the system font stack in CSS

The system font stack selects the native UI font for each operating system:

body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
"Helvetica Neue", Arial, sans-serif;
}

This renders in San Francisco on Apple devices, Segoe UI on Windows, and the platform default sans-serif on Linux and Android, all without a network request.

Anchor to Configure system fonts in theme settingsConfigure system fonts in theme settings

The Shopify font picker setting lets merchants choose fonts. The default value is required, and it must be a real font handle: system isn't one. System fonts have their own handles, such as system_ui_n4, sans_serif_n4, serif, and mono.

A handle identifies one variant rather than a family, so the suffix only appears when the family has more than one variant. system_ui and sans_serif each ship four variants, named with a style letter and a weight digit: n4 (normal 400), i4 (italic 400), n7 (normal 700), and i7 (italic 700). serif and mono ship a single regular variant, so their handles have no suffix.

To default to the system UI font, use system_ui_n4 in your theme's settings_schema.json:

config/settings_schema.json

{
"name": "Typography",
"settings": [
{
"type": "font_picker",
"id": "type_body_font",
"label": "Body font",
"default": "system_ui_n4"
},
{
"type": "font_picker",
"id": "type_header_font",
"label": "Heading font",
"default": "system_ui_n4"
}
]
}

When the merchant's selected font is the system font, output the system font stack instead of a web font:

layout/theme.liquid

{%- assign body_font = settings.type_body_font -%}

{% style %}
{{ body_font | font_face: font_display: 'swap' }}

:root {
{%- if body_font.system? -%}
--font-body-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
Roboto, "Helvetica Neue", Arial, sans-serif;
{%- else -%}
--font-body-family: {{ body_font.family }}, {{ body_font.fallback_families }};
{%- endif -%}
}
{% endstyle %}

The font_face filter outputs a CSS @font-face declaration, not an HTML tag, so it has to sit inside a stylesheet. The style tag wraps its contents in a <style> element for you. Emitting font_face outside a stylesheet prints the declaration into the document as text and the font never registers.

When the selected font is a system font, font_face returns an empty string, because system fonts have no files on the CDN. That's why the call doesn't need a system? guard.

Then use the CSS variable throughout your theme:

body {
font-family: var(--font-body-family);
}

Anchor to Skip font preloads for system fontsSkip font preloads for system fonts

When settings.type_body_font.system? is true, don't preload a font file. font_url returns an empty string for system fonts, so an unguarded preload emits a tag with an empty href. Guard every preload and preconnect with a system? check, as shown below.


Anchor to Checking whether the selected font is a system fontChecking whether the selected font is a system font

Use the .system? property on the font object to branch your template logic:

{%- assign header_font = settings.type_header_font -%}

{%- unless header_font.system? -%}
{%- comment -%} preload_tag outputs an HTML tag, so it belongs outside the stylesheet {%- endcomment -%}
{{ header_font | font_url | preload_tag: as: 'font', type: 'font/woff2' }}
{%- endunless -%}

{% style %}
{{ header_font | font_face: font_display: 'swap' }}

:root {
{%- if header_font.system? -%}
--font-heading-family: system-ui, -apple-system, sans-serif;
{%- else -%}
--font-heading-family: {{ header_font.family }}, {{ header_font.fallback_families }};
{%- endif -%}
}
{% endstyle %}

font_url takes an optional positional format argument, woff2 (the default) or woff. It has no variant parameter: to preload a different weight or style, pipe the font through font_modify first. Don't pass crossorigin to preload_tag, because it adds crossorigin="anonymous" on its own when as is font.

Dawn implements this pattern in its theme.liquid head. It calls font_face unconditionally inside a {% style %} block, because the filter returns nothing for system fonts, and it guards the things that would otherwise emit a dead request: the preconnect to fonts.shopifycdn.com and each preload are wrapped in {%- unless ... .system? -%}. When the merchant selects a system font, no font requests are made at all.


  • Chrome DevTools Network panel: filter by Font. With system fonts configured, the waterfall should show no font requests at all: no requests to fonts.shopifycdn.com, no /cdn/fonts/ requests on your store domain, and no font files from your theme's assets/ directory.
  • CLS measurement: use the Performance panel's Layout Shifts track to confirm that no layout shift occurs during or after initial render when system fonts are active.
  • FCP comparison: compare FCP with and without system fonts using Performance > Insights. System fonts improve FCP by eliminating the font download from the critical path.
  • Visual review: inspect the rendered page across operating systems, such as macOS, Windows, and Android, to confirm that the system font stack provides acceptable typographic quality.


Was this page helpful?