Fonts & Web Fonts
The font is the voice of your design. A school site, a law firm and a kids' game should not sound the same. But fonts also affect speed and readability: every web font is a file the visitor must download, and a badly loaded font can make text invisible or make the page jump.
In this lesson you will build font stacks, set size, weight and style, load your own fonts with @font-face, control loading with font-display, and use variable fonts.
Font stacks
font-family takes a list of fonts, separated by commas. The browser uses the first one that is available and falls back to the next. This list is called a font stack. Always end it with a generic family, a keyword that tells the browser what kind of font to use if nothing else matches.
| Generic family | What it gives you |
|---|---|
system-ui | The operating system's interface font (Segoe UI on Windows, San Francisco on Apple devices, Roboto on many Android phones) |
sans-serif | A font without small strokes on the letter ends |
serif | A font with small strokes, common in books and newspapers |
monospace | Every character the same width; good for code |
Font names that contain spaces need quotes, such as "Times New Roman". Generic family names must not be quoted.
The fallback even works letter by letter. If your first font has no Devanagari characters, the browser finds another font for the Hindi text and keeps using your first choice for the English.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Font stacks</title>
<style>
body { padding: 1rem; font-size: 1.125rem; }
.ui { font-family: system-ui, sans-serif; }
.serif { font-family: Georgia, "Times New Roman", serif; }
.mono { font-family: ui-monospace, Consolas, monospace; }
.w300 { font-weight: 300; }
.w700 { font-weight: 700; }
em { font-style: italic; }
</style>
</head>
<body>
<p class="ui">System UI: Kabir paid ₹1,299 for a cricket bat.</p>
<p class="serif">Serif: Kabir paid ₹1,299 for a cricket bat.</p>
<p class="mono">Monospace: Kabir paid ₹1,299 for a cricket bat.</p>
<p class="ui w300">Light (300): <em>shabash</em>, well played!</p>
<p class="ui w700">Bold (700): <em>shabash</em>, well played!</p>
<p class="serif" lang="hi">Serif with Hindi: नमस्ते, आपका स्वागत है</p>
</body>
</html>Each line uses a different stack. The last line asks for Georgia, which has no Hindi letters, so the browser quietly uses a Devanagari font for the Hindi words. The exact fonts you see depend on your device, which is exactly why stacks end with a generic family.
system-ui, sans-serif is a great default for interfaces: it looks native on every device and costs nothing to download.
Size, weight and style
font-size: useremso text respects the reader's settings (see Units & Sizing Functions).font-weight: numbers from 100 (thin) to 900 (black).normalis 400 andboldis 700. A font can only show the weights it actually has.font-style:normaloritalic.line-height: covered in Styling Text.
The font shorthand sets several of these at once. It must include at least the size and the family, and any part you leave out is reset. Run this example and compare the two forms:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>font shorthand</title>
<style>
body {
font: 400 1.125rem/1.6 Georgia, serif; /* weight, size/line-height, family */
padding: 1rem;
}
.fixed button,
.fixed input {
font: inherit; /* form controls do not inherit fonts by default */
}
</style>
</head>
<body>
<form>
<p>Default controls: <input placeholder="Your city"> <button type="button">Search</button></p>
</form>
<form class="fixed">
<p>With font: inherit: <input placeholder="Your city"> <button type="button">Search</button></p>
</form>
</body>
</html>In the first form, the input and button use the browser's own small font, which clashes with the page. In the second, font: inherit makes them match the surrounding text.
Web fonts with @font-face
To use a font that is not installed on the visitor's device, you host the font file and describe it with an @font-face rule. Then you use its name in font-family like any other font.
@font-face {
font-family: "Site Sans";
src: url("/fonts/site-sans-variable.woff2") format("woff2");
font-weight: 100 900; /* a variable font: every weight in one file */
font-style: normal;
font-display: swap;
}
body {
font-family: "Site Sans", system-ui, sans-serif;
}font-familyhere is the name you choose for the font.srcpoints to the file. Use the WOFF2 format: it is compressed and supported by all current browsers, so you do not need older formats.font-weightandfont-styledescribe what this file contains.font-displaycontrols what happens while the file downloads.
Keep the fallback fonts in the stack. They are used while the web font loads and if it fails.
font-display
Until a web font arrives, the browser has to decide what to show. font-display tells it:
| Value | Behaviour |
|---|---|
swap | Show the fallback font at once, swap in the web font when it arrives |
optional | Use the web font only if it arrives very quickly; otherwise keep the fallback for this visit |
block | Hide the text for a short time while waiting |
fallback | A short hiding period, then the fallback; swap only if the font arrives soon |
swap is the common choice: text is readable at once. Its downside is a visible jump when the fonts change, so pick a fallback font with a similar size. optional avoids the jump entirely and suits slow mobile connections, at the cost of sometimes not showing your font on the first visit.
Loading fonts from a font service
Services such as Google Fonts give you a <link> to paste into your <head>. The preconnect hints let the browser start connecting to the font servers early:
<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=Poppins:wght@400;600&display=swap">The display=swap part sets font-display: swap for you. Self-hosting the WOFF2 files on your own server is often faster and avoids sending visitor data to a third party, so many teams download the files and write their own @font-face rules.
Variable fonts
A traditional font family needs a separate file for every weight and style: regular, medium, semibold, bold, italic... A variable font packs a whole range into one file. You describe the range in @font-face (like font-weight: 100 900 above), and then any value in that range works, even in-between ones like font-weight: 650.
Variable fonts can also vary other axes, such as width or slant. For the standard axes, use the normal properties: font-weight, font-style and font-stretch. Some fonts add their own custom axes, which you set with font-variation-settings and a four-letter axis tag from the font's documentation.
Common mistakes
- No generic family at the end. If the web font fails, the browser falls back to its default, which may look very different.
- Quoting generic names.
"sans-serif"in quotes looks for a font literally called sans-serif. Writesans-serif. - Using weights the font does not have. Asking for 300 when the font only has 400 and 700 gives you a substitute or a faked style.
- Loading too many fonts. Five families and ten weights can add a lot to every page load.
- Forgetting
font: inheriton form controls. Buttons and inputs then use a different font from the page.
Key points
font-familytakes a stack of fonts; always end it with a generic family such assans-serif.system-ui, sans-serifgives a native look with no download.- Use
remforfont-size; weights run from 100 to 900, with 400 normal and 700 bold. @font-faceloads your own font files; use the WOFF2 format.font-display: swapshows readable text immediately while the web font loads.- A variable font gives you a range of weights and other axes from one file.
Exercise
Write the CSS for a college website. Create an @font-face rule for a variable font file at /fonts/campus-variable.woff2 covering weights 200 to 800, with font-display: swap. Use it for the body with a sensible fallback stack, set headings to weight 650, and make sure buttons and inputs use the same font as the page.
Show solution
@font-face {
font-family: "Campus";
src: url("/fonts/campus-variable.woff2") format("woff2");
font-weight: 200 800;
font-style: normal;
font-display: swap;
}
body {
font-family: "Campus", system-ui, sans-serif;
font-size: 1rem;
line-height: 1.6;
}
h1, h2, h3 {
font-weight: 650;
}
button, input, select, textarea {
font: inherit;
}Because the font is variable and covers 200 to 800, the in-between weight 650 is drawn exactly, with no extra file.
Quick Quiz
1. Why should a font stack end with a generic family like sans-serif?
If every named font is missing, the generic family still gives a font of the right kind.
2. Which font-display value shows the fallback font immediately and swaps when the web font arrives?
swap gives readable text at once and replaces the font when the file has loaded.
3. Which font file format should you use for web fonts today?
WOFF2 is compressed and supported by all current browsers.
4. What is the main benefit of a variable font?
Variable fonts store a continuous range of styles in a single file.
Learn CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
