WordPress themes ship with a small set of fonts their authors chose, and for a lot of sites that is fine. But at some point most people want something specific – a brand font, a font that matches the logo, a typeface the designer picked, or just something that does not look like every other WordPress site. The path from “I want this font” to “the font is live on my site” depends on what kind of theme you are running and where the font comes from, and picking the wrong path wastes an afternoon or leaves you with a site that loads the font from three different places at once.
This guide walks through three methods in order of simplicity – theme settings, block theme
theme.json
, and fully self-hosted fonts – explains when each is the right choice, and covers the GDPR and performance reasons people increasingly self-host fonts that used to come from Google.
The three methods at a glance#
- Theme settings (classic themes). Most classic themes have a font picker in the Customizer, often backed by Google Fonts. You pick a font from a dropdown and the theme handles loading. Zero code.
-
theme.json(block themes). Block themes declare their fonts in a JSON file. You either use the Appearance > Editor > Styles > Typography interface, or edittheme.jsondirectly to add or change fonts. Still mostly no-code. - Self-hosted with
@font-face. You download the font files (.woff2), drop them in your theme, and register them with CSS. Works with any theme, any font source, and is the right choice when you need full control, GDPR compliance, or to use a font that Google Fonts does not carry.
Pick the first method that covers your case. Do not jump to self-hosting because it sounds more professional – if the theme’s built-in picker works, use the built-in picker.
Method 1: Google Fonts via theme settings (classic themes)#
Most classic themes – Astra, GeneratePress, OceanWP, Kadence in classic mode, Divi, Avada, and anything based on a Bootstrap-style starter – expose a font selector in the WordPress Customizer.
- In the WordPress admin, go to Appearance > Customize
- Look for a section called Typography, Fonts, or Global Styles
- Click into the section and you will see per-element font controls (body, headings, buttons, menu items)
- Pick a font from the dropdown – the theme usually ships with the entire Google Fonts library available, sometimes plus a few system fonts
- Click Publish
The theme handles the rest. It generates the appropriate
<link>
tag or
@font-face
declaration and outputs it on every page. No code, no file uploads, no functions.php edits.
When this works well. The font you want is in Google Fonts (there are over 1,500 of them, so this covers most cases), the theme’s typography controls expose the element you want to change, and you are OK with Google Fonts’ CDN delivering the file. For the vast majority of WordPress sites, this is the right answer.
When to skip to a different method. Your font is not in Google Fonts. You need GDPR compliance for an EU-facing site (covered below). The theme’s picker does not expose the element you want to change. Your theme is a block theme (use method 2). You need to match a specific brand font that was licensed from a foundry like Adobe Fonts, MyFonts, or Monotype.
Gotcha: font loading in classic themes. Many classic themes still use the Google Fonts CSS API (
https://fonts.googleapis.com/css?family=...
), which means every visitor makes a DNS lookup to Google, an HTTPS handshake, a CSS fetch, and then fetches the
.woff2
files from
fonts.gstatic.com
. This is slower than self-hosting (two extra network connections), and it is what creates the GDPR problem. Some themes (Astra, GeneratePress) have a “local Google Fonts” option in settings that downloads the font into the uploads directory and serves it from your own domain – if you see that option, turn it on.
Method 2: Google Fonts in block themes via theme.json#
If your site uses a block theme – Twenty Twenty-Four, Twenty Twenty-Five, Spectra One, Frost, or any theme that uses the Full Site Editor – fonts are managed in
theme.json
and through the Styles interface in Appearance > Editor.
The visual approach
- Go to Appearance > Editor
- Click Styles in the top-right
- Click Typography
- Click on an element (Body, Headings, Links, Captions, Buttons) and pick a font
Block themes typically include a handful of fonts out of the box. To add new ones without touching code, WordPress 6.5+ includes a Font Library: in the Styles panel, click Typography, then the Manage fonts icon (the
+
next to the fonts list). You can install Google Fonts directly from the library or upload
.woff2
files from your computer.
The Font Library writes installed fonts into your site’s database and serves them from the uploads directory – they are effectively self-hosted, with no ongoing connection to Google once downloaded. This is the cleanest no-code path on a modern block theme.
The
theme.json
approach
If you are building a child theme or prefer to manage fonts in code,
theme.json
has a
settings.typography.fontFamilies
array. Each entry registers a font family and optionally the files that implement it:
{
"$schema": "https://schemas.wp.org/wp/6.7/theme.json",
"version": 3,
"settings": {
"typography": {
"fontFamilies": [
{
"name": "Inter",
"slug": "inter",
"fontFamily": "Inter, system-ui, sans-serif",
"fontFace": [
{
"fontFamily": "Inter",
"fontWeight": "400",
"fontStyle": "normal",
"src": ["file:./assets/fonts/Inter-Regular.woff2"]
},
{
"fontFamily": "Inter",
"fontWeight": "700",
"fontStyle": "normal",
"src": ["file:./assets/fonts/Inter-Bold.woff2"]
}
]
}
]
}
}
}
The
file:
prefix tells WordPress the font is self-hosted. Drop the
.woff2
files into
wp-content/themes/your-theme/assets/fonts/
and WordPress generates the
@font-face
CSS automatically. No
functions.php
edits needed.
If you just want to reference a Google-hosted font instead, omit
fontFace
and use the full CSS
fontFamily
value as you would in any stylesheet. The visual editor will list the family as a choice everywhere fonts can be picked.
Font weights. Each weight is a separate
fontFace
entry. If you only register 400 and 700, the browser will synthesize 500 and 600 by faking the rendering, which almost always looks bad. Either register every weight you use, or register just the weights you use and only apply those weights in your CSS.
fontFamily
stack. Always include a fallback.
"fontFamily": "Inter, system-ui, sans-serif"
means if Inter has not loaded yet, the browser uses the OS default sans-serif. This prevents the “invisible text” problem during the font load – the page shows text in the fallback font, then swaps to Inter when it arrives. Pair this with
font-display: swap
in your
@font-face
(WordPress does this automatically when you use
file:
in
theme.json
) and the behaviour is correct out of the box.
Method 3: Self-hosting custom fonts with @font-face#
If neither of the above covers your case – classic theme without a built-in loader, font that is not in Google Fonts, GDPR-driven decision to drop Google entirely, or a licensed brand font – you self-host the font files and register them with CSS yourself. This is the most work but also the approach that gives you full control.
Step 1: Get the font files
You need
.woff2
at minimum.
.woff2
is supported in every browser in current use (even Safari on iOS 10+ handles it), and it is the smallest of the modern formats thanks to Brotli compression.
.ttf
and
.otf
are the original desktop formats; they work, but they are roughly twice the size of the equivalent
.woff2
. Older
.woff
is unnecessary on a site that does not need to support IE11.
For a licensed foundry font, the vendor provides the files directly after purchase. For a free font from Google Fonts that you want to self-host, use the
google-webfonts-helper
tool (gwfh.mranftl.com) – it generates the exact
.woff2
files and CSS you need, configured for the weights and character sets you pick. For a font you already have as
.ttf
or
.otf
, convert it to
.woff2
using
woff2_compress
(the official Google tool) or any of the web-based converters.
Do not serve
.ttf
or
.otf
from the public internet if you can avoid it. They work, but they are unnecessarily large and the performance cost compounds across pages.
Step 2: Upload the fonts to your theme
Create a
fonts
directory inside your theme (or child theme) and put the
.woff2
files in it:
wp-content/themes/your-theme/
├── assets/
│ └── fonts/
│ ├── brandname-regular.woff2
│ └── brandname-bold.woff2
├── functions.php
└── style.css
Put them in a child theme if you are using a third-party parent theme. A theme update will wipe
wp-content/themes/parent-theme/fonts/
but not your child theme’s directory.
Step 3: Register with
@font-face
In your theme’s main stylesheet (or a dedicated
fonts.css
file), add the
@font-face
declarations:
@font-face {
font-family: 'BrandName';
src: url('assets/fonts/brandname-regular.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'BrandName';
src: url('assets/fonts/brandname-bold.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
Key settings to get right:
-
font-display: swapis the single most important line. Without it, browsers can hide text for up to 3 seconds while the font downloads, giving you a blank page for slow connections. Withswap, the fallback font shows immediately and is replaced when the custom font arrives. This is also what Core Web Vitals’ Cumulative Layout Shift grading expects. -
format('woff2')tells the browser to only download this file if it can parse.woff2. Every browser in current use can. - One
@font-faceper weight and style. A separate block for regular, bold, italic, bold-italic. Do not try to combine them into one.
Step 4: Enqueue the stylesheet
If the
@font-face
declarations are in your theme’s main
style.css
, they are already loaded on every page – you are done. If you put them in a separate file, enqueue it from
functions.php
:
function your_theme_enqueue_fonts() {
wp_enqueue_style(
'your-theme-fonts',
get_theme_file_uri('/assets/fonts.css'),
array(),
'1.0'
);
}
add_action('wp_enqueue_scripts', 'your_theme_enqueue_fonts');
Put this in your child theme’s
functions.php
, not the parent theme’s.
get_theme_file_uri()
resolves to the child theme’s URL when called from the child theme.
Step 5: Use the font
Apply it in CSS like any other font:
body {
font-family: 'BrandName', system-ui, sans-serif;
}
h1, h2, h3 {
font-family: 'BrandName', Georgia, serif;
font-weight: 700;
}
Always include a fallback stack. If the
.woff2
fails to load or has not arrived yet, the browser uses the next font in the list. System fonts (
system-ui
,
-apple-system
,
BlinkMacSystemFont
) are the fastest fallback because they require no download.
Preload critical fonts
If your above-the-fold content uses a custom font, preload it so the browser starts fetching it before it would discover the
@font-face
rule in your stylesheet:
function your_theme_preload_fonts() {
echo '<link rel="preload" href="' . get_theme_file_uri('/assets/fonts/brandname-regular.woff2') . '" as="font" type="font/woff2" crossorigin>';
}
add_action('wp_head', 'your_theme_preload_fonts', 1);
Only preload the specific weights used above the fold (usually regular body text and the main heading weight). Preloading every weight wastes bandwidth on fonts that may never be used on a given page. The broader performance tradeoff is covered in how to speed up WordPress.
Why self-host instead of using Google Fonts directly#
Two reasons come up repeatedly: GDPR compliance and performance.
GDPR and the Google Fonts problem
In January 2022, a court in Munich ruled that embedding Google Fonts on a website transmits the visitor’s IP address to Google in the US without a valid legal basis under GDPR, and fined the site operator €100. The ruling itself was small, but it triggered a wave of automated warning letters to site operators across Germany and Austria – often demanding settlements of €100-200 to avoid formal complaints. Similar reasoning has since surfaced in other EU member states.
The core issue is that loading a font from
fonts.googleapis.com
happens before any cookie consent prompt can run. The visitor’s browser connects to Google’s servers and transmits their IP as part of the TCP handshake – that is enough, under the ruling, to constitute personal data processing without consent.
Self-hosting removes this entirely. The font is served from your domain, with no connection to Google at any point. No IP transmission, no consent required, no legal grey area. If your site serves EU visitors and the fonts are fetched from Google’s CDN, self-hosting is now effectively a compliance requirement.
WordPress 6.5’s Font Library (method 2 above) handles this automatically when you install fonts through the library interface – they are stored locally and served from your own domain. Classic themes usually do not handle it automatically; check whether your theme has a “local Google Fonts” option, and if it does not, use a plugin like OMGF (Optimize My Google Fonts) to rewrite the theme’s Google Fonts references to local copies.
Performance
Even without the legal angle, self-hosting is measurably faster:
- One fewer DNS lookup.
fonts.googleapis.comrequires DNS resolution before the browser can fetch the CSS. Self-hosted fonts reuse the DNS entry your site already has. - One fewer TLS handshake. Same reason – you are reusing the HTTPS connection to your own server instead of opening a new one.
- HTTP/2 or HTTP/3 multiplexing. Modern HTTP versions can download multiple files in parallel over the same connection. Loading fonts from a different domain means they compete with assets from your site rather than sharing the connection.
- Better cache control. When you serve the fonts yourself, you set the cache headers. A long
Cache-Control: max-age=31536000on.woff2files means returning visitors do not re-download the fonts. Google Fonts sets a 1-year cache too, but you have no control over it if the policy changes.
The difference on a typical WordPress site is 100-300ms faster First Contentful Paint, which matters both for user experience and for the Core Web Vitals scores that affect search rankings.
Tips for picking and configuring fonts#
- Do not use more than two typefaces. One for body, one for headings, maybe a third for UI elements. Every additional family is an additional download and an additional rendering style the reader has to adjust to.
- Do not use more than four weights total across all families. Each weight is a separate file. Three body weights (regular, bold, italic) and one heading weight cover most designs.
- Use variable fonts where possible. A single variable font file like Inter or Source Sans can replace five or six static weight files. Browser support is universal now. Variable fonts are registered the same way in
@font-facewith afont-weight: 100 900range declaration. - Limit character subsets. If your site is English-only, you do not need the full Unicode range. Google Fonts lets you pick subsets; self-hosted fonts from foundries usually ship as Latin-only already.
- Test on mobile. A font that looks great at 16px on a desktop may look thin and hard to read on a phone. The default body-size increase of mobile browsers means fonts render smaller relative to the viewport.
- Check the license for self-hosted fonts. Free for personal use is not free for commercial use. Most licensed foundry fonts permit self-hosting, but not all – check before uploading.
Common mistakes#
- Loading Google Fonts from the theme and also self-hosting a copy. Check the network tab – many sites end up loading the same font from both sources because the theme’s built-in Google Fonts loader was not disabled when the self-hosted version was added. The fix is usually an option in the theme’s settings to disable the built-in font loader.
- Forgetting
font-display: swap. Without it, browsers block rendering of any text that uses the font for up to three seconds. This is the single biggest font-related cause of the “page loads then jumps” user experience. - Using
.ttfinstead of.woff2. Twice the file size, no compatibility benefit. - Not preloading critical fonts. If your logo or hero headline uses a custom font, preload it. Otherwise the browser discovers it while parsing CSS, which is too late for it to show on the first paint.
- Self-hosting a Google Font for GDPR but leaving the CSS
@importto Google. The font files are local but the CSS still references Google. Check the page source – the stringfonts.googleapis.comshould not appear anywhere. - Uploading fonts to the Media Library. WordPress does not expect font files there, the MIME type checks may reject
.woff2, and the files end up in a location that is not ideal for cache headers. Use the theme directory instead. - Editing the parent theme’s files. An update wipes them. Use a child theme.
- Mixing HTTP and HTTPS font sources. A site served over HTTPS that loads a font from
http://fonts.googleapis.comtriggers a mixed content error. Always use HTTPS, or better, self-host and avoid the issue entirely.
How Hostney handles custom fonts#
Hostney does not impose any special restriction on custom fonts – you can upload
.woff2
files to your theme via the file manager, SFTP, or the WordPress admin, and serve them from your own domain just like any other asset. The
.woff2
MIME type is configured correctly in nginx, cache headers are set to one year on versioned font filenames (which means returning visitors do not re-download them), and the built-in FastCGI caching layer serves the CSS that references your fonts from cache without re-generating it on every request.
For performance, Hostney’s static-asset delivery already does the things a manual self-hosting setup has to be configured to do: gzip and Brotli compression are enabled for text-based assets, HTTP/2 and HTTP/3 serve font files in parallel with CSS and images over the same connection, and long cache headers keep repeat visits fast. The complete WordPress performance guide covers the full stack of optimizations – font loading is one piece of a larger picture that also includes image optimization, caching, and critical CSS.
If you are building on top of a custom WordPress theme or working with a block theme via theme.json, the fonts live alongside the rest of your theme assets and deploy with your theme update workflow. No special configuration needed.
Summary#
Three methods, in increasing order of control and effort: (1) use your theme’s built-in font picker for Google Fonts if it has one, (2) use
theme.json
and the Font Library on block themes, (3) self-host
.woff2
files with
@font-face
for licensed fonts, non-Google sources, GDPR compliance, or maximum performance. Do not skip straight to the hardest option – use the simplest method that covers your case. When you do self-host, the three things that matter most are
font-display: swap
(to avoid invisible text during load), a sensible fallback stack in every
font-family
rule (to survive font load failures), and preloading the one or two fonts used above the fold (to avoid delayed first paint). Everything else is tuning.