In short
Learn to use custom fonts with @font-face: self-host WOFF2 files, control loading with font-display, use variable fonts, preload fonts and reduce layout shift. How do you self-host a font with @font-face? @font-face tells the browser: "Here is a font file.
Learn to use custom fonts with @font-face: self-host WOFF2 files, control loading with font-display, use variable fonts, preload fonts and reduce layout shift.
How do you self-host a font with @font-face?
@font-face tells the browser: "Here is a font file. When I say this family name, use it." Self-hosting means the font files live on your own server, next to your CSS, instead of coming from a service like Google Fonts. It is faster (no extra connection to another server), works offline in apps and is better for privacy.
Use the WOFF2 format. It is a compressed font format that every modern browser supports, and it is much smaller than old TTF or OTF files.
@font-face {
font-family: "Nunito";
src: url("/fonts/nunito-regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: "Nunito";
src: url("/fonts/nunito-bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}
body {
font-family: "Nunito", system-ui, sans-serif;
}
h1, strong {
font-weight: 700;
}
We registered two files under the same family name, one with weight 400 and one with 700. When text is normal, the browser uses the regular file. When it is bold, it automatically picks the bold file. If you skip the bold file, the browser makes "fake bold" by smearing the letters, which looks worse.
Tip: Only load the weights you really use. Each file is another download. Two or three weights are enough for most sites.
What does font-display do?
Fonts take time to download. What should the browser show in the meantime? font-display decides:
| Value | What happens |
|---|---|
swap | Show a fallback font right away, swap to your font when it arrives. |
optional | Use your font only if it loads very fast; otherwise keep the fallback for this visit. |
fallback | Hide text for a very short moment, then fallback; swap only if the font arrives soon. |
block | Hide the text for up to about 3 seconds while waiting. |
For body text, swap is the common choice: people can start reading immediately. optional is great for performance, because the page never "jumps" after loading. Avoid block for normal text.
Variable fonts: many weights in one file
A variable font is a single font file that contains a whole range of styles. Instead of separate files for 400, 500, 600 and 700, one file can do every weight from 100 to 900, and anything in between, like 650. It is like a dimmer switch instead of an on/off switch.
@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;
font-weight: 400;
}
h1 {
font-weight: 780;
}
.nav a:hover {
font-weight: 600;
}
.condensed-label {
font-variation-settings: "wdth" 80;
}
Notice font-weight: 100 900 with two numbers: it tells the browser this one file covers the whole range. Headings now use weight 780, which would be impossible with normal font files. Each variable font has axes, the things you can change. Common ones are wght (weight), wdth (width), slnt (slant) and opsz (optical size).
Common mistake: Using
font-variation-settings: "wght" 700for weight. It works, but it overrides inheritance in strange ways and<strong>stops getting bolder. Use the normal properties when they exist:font-weightforwght,font-stretchforwdth,font-styleforslnt. Keepfont-variation-settingsfor custom axes.
Preloading an important font
Normally, the browser only discovers a font after it has downloaded the HTML, then the CSS, and then sees text that needs it. That is late. A preload link in the HTML <head> says "start downloading this now".
<head>
<link rel="preload" href="/fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin>
<link rel="stylesheet" href="/css/style.css">
</head>
The font starts downloading at the same time as the CSS, so it arrives sooner and the swap happens earlier or not at all. The crossorigin attribute is required for fonts, even on your own domain; without it the browser downloads the font twice.
Preload only one or two fonts that are used above the fold (the part visible without scrolling). Preloading everything makes everything slower.
Reducing layout shift with size-adjust
When the font swaps, the text often changes size a little, because the fallback (like Arial) has a different width and height. Lines rewrap and the page jumps. This is called layout shift, and it annoys users. You can make the fallback font match your custom font more closely with size-adjust and a few related descriptors.
@font-face {
font-family: "Inter Fallback";
src: local("Arial");
size-adjust: 107%;
ascent-override: 90%;
descent-override: 22%;
line-gap-override: 0%;
}
body {
font-family: "Inter", "Inter Fallback", sans-serif;
}
We created a new font family that is really Arial from the user's computer (local()), but scaled by 107% and with adjusted line height values. Now Arial takes up almost the same space as Inter, so when Inter arrives, the text barely moves. The exact numbers depend on your font; tools such as Fontaine or the Next.js font tools can calculate them for you.
Learn all the descriptors on MDN's @font-face page.
Practice exercise
Download a free variable font (for example from Google Fonts, using the "Download family" button) and convert or pick the WOFF2 file. Self-host it with @font-face, declare its weight range, use three different weights on your page, preload it, and add a size-adjusted fallback.
Hint: open DevTools, go to the Network tab and filter by "Font" to check that your font loads only once and early.
Key takeaways
@font-faceregisters your own font files; use WOFF2 and self-host when you can.- Use the same
font-familyname with differentfont-weightvalues for each file. font-display: swapshows text right away;optionalavoids late swaps.- Variable fonts cover a range like
font-weight: 100 900in one file. - Preload one key font with
crossorigin, and usesize-adjustto reduce layout shift.