Fonts & Web Fonts

CSS · Lesson 15 of 55 8 min read Updated

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 familyWhat it gives you
system-uiThe operating system's interface font (Segoe UI on Windows, San Francisco on Apple devices, Roboto on many Android phones)
sans-serifA font without small strokes on the letter ends
serifA font with small strokes, common in books and newspapers
monospaceEvery 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.

HTMLTry it yourself
<!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: use rem so text respects the reader's settings (see Units & Sizing Functions).
  • font-weight: numbers from 100 (thin) to 900 (black). normal is 400 and bold is 700. A font can only show the weights it actually has.
  • font-style: normal or italic.
  • 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:

HTMLTry it yourself
<!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.

CSS
@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-family here is the name you choose for the font.
  • src points to the file. Use the WOFF2 format: it is compressed and supported by all current browsers, so you do not need older formats.
  • font-weight and font-style describe what this file contains.
  • font-display controls 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:

ValueBehaviour
swapShow the fallback font at once, swap in the web font when it arrives
optionalUse the web font only if it arrives very quickly; otherwise keep the fallback for this visit
blockHide the text for a short time while waiting
fallbackA 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:

HTML
<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. Write sans-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: inherit on form controls. Buttons and inputs then use a different font from the page.

Key points

  • font-family takes a stack of fonts; always end it with a generic family such as sans-serif.
  • system-ui, sans-serif gives a native look with no download.
  • Use rem for font-size; weights run from 100 to 900, with 400 normal and 700 bold.
  • @font-face loads your own font files; use the WOFF2 format.
  • font-display: swap shows 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
CSS
@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?

2. Which font-display value shows the fallback font immediately and swaps when the web font arrives?

3. Which font file format should you use for web fonts today?

4. What is the main benefit of a variable font?

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