Module 4: Typography & Text Styling
Fonts: Stacks, Web-Safe Choices and Loading Custom Fonts
How font-family fallbacks work, which fonts you can count on, and how to load Google Fonts or your own files without wrecking performance.
A font choice is a request, not a guarantee. You ask for a font, and if the visitor's device does not have it, the browser moves down your list. Understanding that list, and the cost of downloading fonts, is what separates a site that looks the same everywhere from one that shifts and flashes as it loads.
font-family is a priority list#
body {
font-family: "Inter", "Helvetica Neue", Arial, sans-serif;
}
The browser tries each family in order and uses the first one that is available, character by character. If none match, the last entry, a generic family, guarantees something sensible.
There are five generic families: serif, sans-serif, monospace, cursive and fantasy, plus modern ones like system-ui, ui-monospace and ui-rounded. Always end your stack with one of them.
Rules for writing the value:
- Names with spaces need quotes:
"Times New Roman". - Generic families are never quoted.
"sans-serif"in quotes would look for a font literally named that. - Put your preferred font first, then close matches, then the generic family.
Web-safe fonts#
Web-safe fonts are the ones installed on nearly every operating system. They need no download, so they render instantly.
| Style | Typical choices |
|---|---|
| Sans-serif | Arial, Helvetica, Verdana, Tahoma, Trebuchet MS |
| Serif | Georgia, Times New Roman, Palatino |
| Monospace | Courier New, Consolas, Menlo |
"Safe" is relative. Helvetica is on macOS and not on Windows, which usually substitutes Arial. That is why stacks list both.
A good zero-download option is the system font stack, which uses whatever the operating system uses for its own interface:
body {
font-family: system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
}
It looks native on every device, loads instantly, and has no layout shift. Many serious sites, including GitHub for years, have used it.
Loading Google Fonts#
Google Fonts hands you a link tag. Put it in the head, before your own stylesheet:
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap"
rel="stylesheet">
body { font-family: "Inter", system-ui, sans-serif; }
What each piece does:
preconnectopens the network connection early, so the font file starts downloading sooner. The first request goes to the stylesheet host and the second to the file host.wght@400;600;700asks for only three weights. Every weight is a separate file. Loading eight when you use three wastes bandwidth.display=swaptells the browser to show text immediately in a fallback font and swap when the real font arrives.
You can also load it from CSS with @import url(...), but, as you saw in Module 1, that adds a sequential request. The link is faster.
Self-hosting with @font-face#
Self-hosting removes a third-party request, avoids privacy concerns about sending visitors' IP addresses to another company, and lets you control caching.
@font-face {
font-family: "Inter";
src: url("/fonts/inter-var.woff2") format("woff2");
font-weight: 100 900; /* variable font: the whole range */
font-style: normal;
font-display: swap;
}
Use woff2. Every current browser supports it, and it compresses better than other formats. The font-family name here is a label you invent. It is what you reference in the rest of your CSS.
Variable fonts#
A variable font packs an entire range of weights, and sometimes widths or slants, into one file. You can then use any value in the range:
h1 { font-weight: 750; }
.label { font-variation-settings: "wdth" 85; }
One variable file is often smaller than three or four static ones, and it gives you smooth control. When a family is available as variable, prefer it.
What happens while a font loads#
A custom font is a download, and the browser has to decide what to show in the meantime. That decision is font-display:
| Value | During loading | Result |
|---|---|---|
block | invisible text for up to ~3s | flash of invisible text |
swap | fallback font right away | visible text, then a swap |
fallback | tiny invisible period, short swap window | a middle ground |
optional | browser may never swap if load is slow | no layout shift, font may not appear |
swap is the common choice. The visual swap, though, can shift the layout if the two fonts have different proportions. This hurts a metric called Cumulative Layout Shift. To reduce it:
- Choose a fallback with similar proportions.
- Use
size-adjust,ascent-overrideand related descriptors in a fallback@font-faceto tune the fallback to match. - Preload your most important font file.
<link rel="preload" href="/fonts/inter-var.woff2" as="font" type="font/woff2" crossorigin>
Font preloads need the crossorigin attribute even for same-origin files, otherwise the browser downloads the file twice.
Subsetting#
A full font includes thousands of glyphs, and your site might only need Latin characters. unicode-range lets the browser download a file only when the page contains characters in that range:
@font-face {
font-family: "Inter";
src: url("/fonts/inter-latin.woff2") format("woff2");
unicode-range: U+0000-00FF;
}
Tools like glyphhanger or pyftsubset can create subsets. For languages with huge character sets, such as Chinese or Japanese, this is the difference between 100 KB and several megabytes.
Common mistakes#
- No generic family at the end. If your font fails, the browser falls back to its default serif.
- Quoting generic names or forgetting to quote names with spaces.
- Loading too many weights and styles. Check the Network tab. If your fonts total 500 KB, trim them.
- Typos in the family name.
font-family: Intrfalls through silently. - Skipping
font-display, which can make text invisible during loading. - A font that looks great on your machine only, because it is installed locally and not actually loaded for visitors. Test with the font uninstalled.
- Using
<link>Google Fonts and then also@importfor the same family.
Practice#
- Set a body font stack with your own preferred font first, two fallbacks and a generic family. In DevTools, open the Computed tab and look at the "Rendered Fonts" section to see which one the browser used.
- Load one Google font with exactly two weights. Check the Network tab and note the file count and size.
- Download the same font as woff2, self-host it with
@font-face, and compare load behavior with throttling set to "Slow 4G". - Switch
font-displaybetweenblockandswapand watch what happens to the text during the slow load.