Topic 4.2
Web Fonts
In one line
@font-face lets a page download its own font files so every visitor sees the same typeface; font-display, the WOFF2 format, subsetting and preloading keep that download from making text invisible or slow.
Think of it like this
Bringing your own pens to an exam. The school (the visitor's device) has some pens, but you want your favourite one, so you carry it in your bag. A web font is a font file the page brings along: the browser downloads it from your server, so the text looks the same on every device. Carrying it costs a little effort (download time), so you pack light.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Web font
- A font file the page downloads, instead of relying on fonts installed on the device.
- @font-face
- The CSS at-rule that declares a web font: its name, its file and which weight/style it is.
- At-rule
- A CSS statement starting with
@, like@font-faceor@media, that gives the browser an instruction. - WOFF2
- The compressed font file format made for the web. Use it for every web font today.
- font-display
- A descriptor that decides what text looks like while the font file is still downloading.
- FOIT
- Flash of invisible text: text is hidden until the font arrives.
- FOUT
- Flash of unstyled text: text shows in a fallback font first, then changes to the web font.
- Subsetting
- Removing characters you don't need from a font file to make it smaller.
- Variable font
- One font file that contains a whole range of weights (and sometimes widths) instead of one file per weight.
- Preload
- A
<link rel="preload">hint telling the browser to start downloading a file early.
Step by step
01Put the font file on your site
Download the font in WOFF2 format (for example from Google Fonts or the font's own site) and put it in a folder such as /fonts. Check the licence allows web use.
02Declare it with @font-face
Give it a name and point at the file. The name is yours to choose; it only has to match what you write in font-family later.
@font-face {
font-family: "Inter";
src: url("/fonts/inter-regular.woff2") format("woff2");
font-weight: 400;
font-style: normal;
font-display: swap;
}03Add the bold face with the same name
A second rule with the same family name and font-weight: 700 tells the browser which file to use for bold text. Without it, the browser fakes bold by thickening the regular letters, which looks smudgy.
@font-face {
font-family: "Inter";
src: url("/fonts/inter-bold.woff2") format("woff2");
font-weight: 700;
font-style: normal;
font-display: swap;
}04Use it in a stack
Use the name like any font, with fallbacks. While the file downloads (and if it fails), the fallbacks are used.
body {
font-family: "Inter", system-ui, sans-serif;
}05Understand the loading timeline
The browser finds the font is needed only after it has the HTML and CSS and knows some text uses it. Then it downloads the file. font-display decides what happens in the meantime.
06Preload the most important file
A preload in the HTML head starts the download before the CSS is even read. crossorigin is required for fonts, even on your own domain, because fonts are always fetched in CORS mode; without it the preloaded file isn't reused and downloads twice.
<link rel="preload" href="/fonts/inter-regular.woff2" as="font" type="font/woff2" crossorigin>Try it yourself
- 1
Change the font-display value
In the first example, change
swaptooptionaland run. Predict the second console line before you look. The CSSOM reports exactly what you wrote. - 2
Point at a font you don't have
In the first example, change both
local(...)names tolocal("Not A Real Font"). Predict what the heading looks like. Nothing insrcloads, so the browser moves on to the next font in the stack:serif. - 3
Watch real fonts load
Open any site that uses web fonts, open DevTools → Network, filter by Font, and reload. You'll see each
.woff2file, its size and when it started. Then set throttling to Slow 4G and reload to see the FOUT or FOIT with your own eyes.
Code & diagrams
In real projects `src` points at a `.woff2` file with `url()`. The preview can't load files, so this uses `local()`. Rename the family in both places and it still works: the name is yours to choose.
Preview
Console
Nothing logged yet.Expected console output
Found a CSSFontFaceRule
font-display: swap`document.fonts` is the page's list of declared faces. With `url()` sources, only faces that some text uses are downloaded; "Unused Font" would never be fetched.
Preview
Console
Nothing logged yet.Expected console output
Faces declared: 2
Shown Font is used by .used: true<!-- 1. Link Google's stylesheet (quick to set up) -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap">
<!-- 2. Self-host: download the .woff2 files, then write your own @font-face -->
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
<style>
@font-face {
font-family: "Inter";
src: url("/fonts/inter-var.woff2") format("woff2");
font-weight: 100 900; /* a variable font: every weight in one file */
font-display: swap;
}
body { font-family: "Inter", system-ui, sans-serif; }
</style>/* Latin letters: downloaded for almost every page */
@font-face {
font-family: "Noto Sans";
src: url("/fonts/noto-latin.woff2") format("woff2");
unicode-range: U+0000-00FF, U+0131, U+0152-0153;
font-display: swap;
}
/* Devanagari (Hindi): downloaded only if the page contains Hindi text */
@font-face {
font-family: "Noto Sans";
src: url("/fonts/noto-devanagari.woff2") format("woff2");
unicode-range: U+0900-097F;
font-display: swap;
}Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Different names in @font-face and font-family
Declare font-family: "Inter"; in @font-face, but write font-family: "Inter Regular", sans-serif; on body.
Break #2
Preload without crossorigin
Write <link rel="preload" href="/fonts/inter.woff2" as="font" type="font/woff2"> with no crossorigin attribute.
Break #3
Load every weight "just in case"
Link a Google Fonts URL with nine weights and their italics, but use only regular and bold on the page.
Myth vs fact
Myth
Declaring a web font downloads it immediately.
Fact
Browsers download a face only when some text on the page needs it. Unused @font-face rules cost nothing but the CSS bytes. Preloading is how you start a download early on purpose.
Myth
font-display: swap is always best.
Fact
swap keeps text visible but can cause a visible jump (layout shift) when the font arrives late. optional avoids the jump and suits body text on slow networks; swap suits brand headings that must use the font.
Myth
Google Fonts is always faster because it's on a CDN.
Fact
Browsers partition their cache by site since about 2020, so another site's cached Google Fonts file isn't reused on yours. Self-hosting removes the extra connection to another domain and is often faster.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Font swaps cause Cumulative Layout Shift (a Core Web Vitals metric) when the fallback and the web font have different metrics. The
@font-facedescriptorssize-adjust,ascent-override,descent-overrideandline-gap-overridelet you define a tuned fallback face (for examplesrc: local("Arial")withsize-adjust: 107%) that occupies almost the same space as the web font. - ▸
The
font-displayperiods come from the CSS Fonts Level 4 spec: a block period (text drawn invisible), then a swap period (fallback shown, swap allowed), then a failure period (stay on the fallback).block≈ 3s block, infinite swap;swap≈ 0 block, infinite swap;fallback≈ 100ms block, ~3s swap;optional≈ 100ms block, no swap. - ▸
The CSS Font Loading API (
document.fonts,new FontFace(),document.fonts.load("1em Inter"),document.fonts.ready) lets JavaScript load fonts on demand and wait for them, which is useful before measuring text or drawing on a canvas. - ▸
Fonts from another origin need the server to send
Access-Control-Allow-Origin; without it the browser refuses to use the file (a CORS error in the console). See the System Design course for how CDNs and caching headers work: fonts should be served with longCache-Controllifetimes and versioned file names.
Remember this
- 1
A web font is declared with the
@font-faceat-rule (a CSS instruction that starts with@). Inside it you give the font a family name of your choice withfont-family, and say where the file is withsrc. After that, you use the name in any normal font stack:font-family: "Inter", system-ui, sans-serif;. - 2
srccan list several sources, tried in order.url("/fonts/inter.woff2") format("woff2")points at a file;local("Inter")uses a copy already installed on the device. WOFF2 (Web Open Font Format 2) is the format to use in 2026: it is compressed with Brotli and supported by every modern browser. WOFF is its older, larger predecessor; raw TTF/OTF files are bigger still; EOT and SVG fonts are obsolete. - 3
One
@font-facerule describes one file, usually one weight and style. To get regular, bold and italic, you write several rules with the samefont-familyname and differentfont-weight/font-styledescriptors. The browser then picks the right file when text is bold or italic. A variable font packs a whole range of weights into one file:font-weight: 100 900;in the rule. - 4
Fonts are downloaded lazily: only when the page actually has text that uses that face. Until the file arrives, the browser must decide what to show. **
font-display** controls this:blockhides text for up to about 3 seconds (a FOIT, flash of invisible text);swapshows a fallback font at once and swaps when the file arrives (a FOUT, flash of unstyled text);fallbackhides for about 100 ms, then shows the fallback and swaps only if the font arrives within about 3 seconds;optionalhides for about 100 ms and uses the font only if it is already there (often cached), never swapping later.autolets the browser decide, which is usually likeblock. - 5
Google Fonts hosts free fonts and writes the
@font-facerules for you. You add a<link>to their stylesheet with&display=swapin the URL and use the family name. It is convenient, but it adds requests to another server (fonts.googleapis.comfor the CSS,fonts.gstatic.comfor the files). Many teams download the files and self-host them for speed and privacy. - 6
Performance rules: use WOFF2 only; load as few weights as possible; subset fonts (keep only the characters you need) and use
unicode-rangeso a file is fetched only when those characters appear; preload the one or two most important files with<link rel="preload" as="font" type="font/woff2" crossorigin>. Font licences matter too: only host fonts whose licence allows web use (Google Fonts are open licences).
Explain it without notes
How do you add a custom web font to a page, step by step?
What do the font-display values block, swap, fallback and optional do, and when would you pick each?
Why is WOFF2 the recommended format, and what other ways can you make fonts load faster?
Why does a font preload need the crossorigin attribute?
Practice
Write @font-face rules for a family called Lato with a regular file /fonts/lato-400.woff2 and a bold file /fonts/lato-700.woff2, both showing fallback text immediately, then use it on body.
Write the HTML line that preloads /fonts/lato-400.woff2 correctly.
Declare a variable font Recursive from /fonts/recursive.woff2 that covers weights 300 to 1000, with font-display: optional.
Trade-offs
- ↔
Web fonts give a consistent brand look but cost download time and can cause invisible text or layout shifts; system fonts cost nothing but vary by device.
- ↔
Google Fonts is quick to set up and serves optimised subsets; self-hosting needs a little work but avoids a third-party connection and keeps visitor data on your site.
- ↔
A variable font is one request for all weights but may be larger than two static files if you only use two weights.
Done when you can
I can write
@font-facerules for regular and bold with one family name.I can explain FOIT and FOUT and choose a
font-displayvalue.I know why WOFF2 is the format to use.
I can preload a font correctly, including
crossorigin.I can add a Google Font with
display=swapor self-host it.I can list at least four ways to make fonts load faster.