Add Custom Fonts With CSS @font-face: WOFF2, Preload and WordPress

Quick Summary
- Upload the font as WOFF2 files, declare each weight with an @font-face rule that uses font-display: swap, and use the family name in font-family; current browsers need no other formats.
- Preload only the font the first screen needs, with the crossorigin attribute; without it, Chrome downloaded the file twice. A variable font covers every weight in one file.
- In WordPress, enqueue a fonts stylesheet and use the wp_preload_resources filter in a classic theme, or declare fontFace in theme.json in a block theme.
To add a custom font to a website, upload the font as a WOFF2 file, declare it with an @font-face rule, and use the name you gave it in font-family. Every current browser supports WOFF2, so you don’t need the EOT, TTF or SVG files that older tutorials list. Add font-display: swap so text shows while the font loads, preload the one font the first screen needs, and in WordPress, load the rule from your child theme or declare the font in theme.json.
The @font-face rule
- Get the font as WOFF2 files, one for each weight and style you use, or a single variable font file (see below). Check that the font’s license allows use on a website.
- Upload them to a folder in your theme, for example
fonts/. - Add this to your stylesheet, with paths relative to the CSS file:
@font-face {
font-family: 'MyWebFont';
src: url('fonts/webfont-400.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'MyWebFont';
src: url('fonts/webfont-700.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}
body {
font-family: 'MyWebFont', system-ui, sans-serif;
}font-familyis the name you use for the font in the rest of your CSS. Give every weight and style the same name, so bold text picks the bold file by itself.srcpoints to the file, relative to the stylesheet.format('woff2')tells the browser the format before it downloads anything. MDN’s compatibility data lists WOFF2 support from Chrome 36, Firefox 39 and Safari 10, so a single WOFF2 source is enough; a WOFF fallback only helps browsers older than those.font-weightandfont-stylesay which face the file contains. The browser only downloads a face when the page uses it: in headless Chrome 153, a page with only regular text loaded the 400 file, and the 700 file was fetched once the page had bold text.font-display: swapshows the text in a fallback font until the web font has loaded, so text never stays invisible. It’s also what PageSpeed Insights’ font display check looks for; fixing “Ensure text remains visible during webfont load” explains the values and how to test them.- The
font-familylist onbodyends with fallbacks.system-uiis the operating system’s interface font, and it’s used while the web font loads or if it fails.
Older tutorials show a longer “bulletproof” version with EOT, TTF and SVG sources. Those lines only served old Internet Explorer versions and early iOS Safari; every current browser supports WOFF2. If you only have a TTF or OTF file, convert it to WOFF2 before you upload it.
Preload the font the first screen needs
A browser only finds out it needs a font after it has downloaded and parsed the CSS and found text that uses it. A preload tag in the <head> starts the download earlier:
<link rel="preload" href="/wp-content/themes/my-theme/fonts/webfont-400.woff2" as="font" type="font/woff2" crossorigin>Preload only the file the first screen depends on, usually the regular weight of the body font. Preloading every weight makes them compete with each other and with your CSS. The crossorigin attribute is required even for fonts on your own domain, because browsers always fetch fonts in CORS mode. In Chrome 153 the font file was downloaded once with crossorigin. Without it, the file was downloaded twice, and Chrome warned: “A preload for ‘…/webfont-400.woff2’ is found, but is not used because the request credentials mode does not match.” Designing pages that feel fast covers which font to preload.
Variable fonts: one file for every weight
A variable font contains a whole range of weights, and sometimes widths, in one file. Declare the range the file covers in the font-weight descriptor:
@font-face {
font-family: 'Inter';
src: url('fonts/inter-variable.woff2') format('woff2');
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
body {
font-family: 'Inter', system-ui, sans-serif;
}Any weight between 100 and 900 now comes from the same file; MDN notes that weights outside the declared range are clamped to it. In Chrome, text set in weights 300 and 800 rendered at different widths, with a single request for the file. It’s one download however many weights you use, but if you only need one or two weights, compare the file sizes, since separate static files can be smaller. Some older examples write format('woff2-variations'); plain format('woff2') works for variable fonts in current browsers.
Self-host the font or use Google Fonts?
- Google Fonts takes one
<link>tag. Adddisplay=swapto its URL, as shown in the font display post. Every visitor’s browser then connects to Google’s servers for the CSS and the font files. - Self-hosting serves the files from your own domain: no connection to another server, your own cache settings, and no request to Google. The old argument that visitors already have Google’s copy cached no longer holds. Since Chrome 86, the browser cache is partitioned by site, so a font cached on one site is downloaded again on the next, and Safari partitions its cache too.
To self-host a Google font, you need its WOFF2 files. The Fontsource npm packages include them, for example @fontsource/roboto, with one file per weight, style and character subset. In WordPress 7.0 and later, Appearance > Fonts can install fonts from Google: WordPress says the fonts “will be downloaded from Google and stored on your site”, and it keeps them in wp-content/uploads/fonts.
How do you add the font in WordPress?
Classic themes
Put the @font-face rules in your child theme’s style.css, with paths relative to that file, or in a separate fonts.css that you enqueue from the child theme’s functions.php:
function my_theme_fonts() {
wp_enqueue_style( 'my-theme-fonts', get_stylesheet_directory_uri() . '/assets/css/fonts.css', array(), '1.0' );
}
add_action( 'wp_enqueue_scripts', 'my_theme_fonts' );The url() paths inside fonts.css are relative to fonts.css, so ../fonts/webfont-400.woff2 finds assets/fonts/. get_stylesheet_directory_uri() points to the child theme; child and parent theme paths explains which function to use when the files are in the parent theme.
To preload the main font, add it with the wp_preload_resources filter, available since WordPress 6.1:
function my_theme_preload_font( $resources ) {
$resources[] = array(
'href' => get_stylesheet_directory_uri() . '/assets/fonts/webfont-400.woff2',
'as' => 'font',
'type' => 'font/woff2',
'crossorigin' => 'anonymous',
);
return $resources;
}
add_filter( 'wp_preload_resources', 'my_theme_preload_font' );WordPress prints the tag at the top of wp_head. Run in WordPress 7.1.2, the two snippets printed a <link rel='stylesheet'> tag for fonts.css?ver=1.0 and <link rel='preload' … as='font' type='font/woff2' crossorigin='anonymous' />. crossorigin='anonymous' does the same as the bare crossorigin attribute.
Block themes: theme.json
A block theme declares its fonts in theme.json, and WordPress writes the @font-face rules itself, on the site and in the editor:
{
"version": 3,
"settings": {
"typography": {
"fontFamilies": [
{
"name": "My Web Font",
"slug": "my-web-font",
"fontFamily": "\"MyWebFont\", system-ui, sans-serif",
"fontFace": [
{
"fontFamily": "MyWebFont",
"fontWeight": "400",
"fontStyle": "normal",
"fontDisplay": "swap",
"src": [ "file:./assets/fonts/webfont-400.woff2" ]
},
{
"fontFamily": "MyWebFont",
"fontWeight": "700",
"fontStyle": "normal",
"fontDisplay": "swap",
"src": [ "file:./assets/fonts/webfont-700.woff2" ]
}
]
}
]
}
}
}file:./is replaced with the URL of the theme folder. WordPress usesget_theme_file_uri()for this, which looks in the child theme first.- Each entry in
fontFacebecomes one@font-facerule. Parsed the way WordPress parses a theme’s settings, this example printed two rules withfont-display:swapand the full URLs to the files. WithoutfontDisplay, WordPress usesfallback, which PageSpeed Insights flags. - The
slugbecomes the CSS variable--wp--preset--font-family--my-web-font, which you can use in your own CSS. - In a child theme, a
fontFamilieslist replaces the parent’s list instead of adding to it, so copy the parent’s fonts into it too. The font display post shows what happened in a test.
Related articles
- adding font-display: swap to @font-face: keeps text visible while the font loads
- preloading the one font that matters: part of making a page feel fast
