CSS3 & Responsive Design

Web Fonts, Google Fonts & Variable Fonts

40 min Lesson 10 of 60

Introduction to Web Fonts

In the early days of the web, designers were limited to a handful of "web-safe" fonts -- typefaces like Arial, Georgia, Times New Roman, and Verdana that were reliably installed on most computers. If you wanted to use any other font, you had to resort to images of text, Flash-based solutions, or JavaScript hacks like Cufon and sIFR. Everything changed with the introduction of the @font-face rule, which allows you to load custom font files directly in the browser. Today, web fonts are a fundamental part of web design, and services like Google Fonts have made it trivially easy to use thousands of high-quality typefaces on any website.

In this lesson, we will explore how web fonts work at a deep technical level. You will learn how to use the @font-face rule, understand web font formats, integrate Google Fonts efficiently, optimize font loading performance, and leverage the power of variable fonts -- one of the most exciting advances in web typography.

The @font-face Rule

The @font-face rule is the CSS mechanism that tells the browser to download and use a custom font file. You declare a font family name, point to one or more font files, and then use that name in your regular font-family declarations throughout your stylesheet.

Basic Syntax

Here is the simplest form of an @font-face declaration:

Example: Basic @font-face Declaration

@font-face {
    font-family: 'MyCustomFont';
    src: url('fonts/my-custom-font.woff2') format('woff2');
}

/* Now use it like any other font */
body {
    font-family: 'MyCustomFont', Arial, sans-serif;
}

@font-face Descriptors

The @font-face rule supports several descriptors that give you fine-grained control over how the font is loaded and used:

  • font-family -- The name you assign to the font. This can be anything you choose; it does not have to match the font's actual name. You use this name in your font-family property elsewhere in your CSS.
  • src -- The source of the font file. This is the most important descriptor. You can specify multiple sources as a comma-separated list, and the browser will use the first one it supports. Each source can use url() for external files or local() for fonts already installed on the user's system.
  • font-weight -- Specifies which weight this font file represents. This does not make the font bold; it tells the browser which file to use when you request a specific weight. For example, if you have separate files for regular and bold, you create two @font-face rules with different font-weight values but the same font-family name.
  • font-style -- Specifies whether this file is for normal, italic, or oblique text. Like font-weight, it maps a style to a specific file.
  • font-display -- Controls how the font behaves while loading (swap, block, fallback, optional, auto). We covered this in detail in the previous lesson.
  • unicode-range -- Limits which characters this font file covers. The browser will only download the file if the page contains characters in the specified range. This is extremely powerful for performance optimization.

Example: Complete @font-face with Multiple Weights and Styles

/* Regular weight */
@font-face {
    font-family: 'Roboto';
    src: local('Roboto'),
         url('fonts/roboto-regular.woff2') format('woff2'),
         url('fonts/roboto-regular.woff') format('woff');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

/* Bold weight */
@font-face {
    font-family: 'Roboto';
    src: local('Roboto Bold'),
         url('fonts/roboto-bold.woff2') format('woff2'),
         url('fonts/roboto-bold.woff') format('woff');
    font-weight: 700;
    font-style: normal;
    font-display: swap;
}

/* Italic style */
@font-face {
    font-family: 'Roboto';
    src: local('Roboto Italic'),
         url('fonts/roboto-italic.woff2') format('woff2'),
         url('fonts/roboto-italic.woff') format('woff');
    font-weight: 400;
    font-style: italic;
    font-display: swap;
}

/* Bold italic */
@font-face {
    font-family: 'Roboto';
    src: local('Roboto Bold Italic'),
         url('fonts/roboto-bold-italic.woff2') format('woff2'),
         url('fonts/roboto-bold-italic.woff') format('woff');
    font-weight: 700;
    font-style: italic;
    font-display: swap;
}

/* Now the browser knows which file to use for each combination */
body {
    font-family: 'Roboto', sans-serif;
    font-weight: 400;  /* Uses roboto-regular.woff2 */
}

strong {
    font-weight: 700;  /* Uses roboto-bold.woff2 */
}

em {
    font-style: italic;  /* Uses roboto-italic.woff2 */
}
Note: By giving all four @font-face rules the same font-family name ("Roboto"), you create a single font family with multiple faces. The browser then automatically picks the correct file based on the font-weight and font-style you use in your CSS. This is much better than creating separate family names like "Roboto-Bold" because it allows bold and italic to work naturally with the <strong> and <em> HTML elements.

The local() Function

The local() function in the src descriptor checks if the font is already installed on the user's system before downloading it. This is a performance optimization -- if the user already has the font, there is no need to download it again.

Example: Using local() for Performance

@font-face {
    font-family: 'Open Sans';
    src: local('Open Sans'),           /* Check installed fonts first */
         local('OpenSans'),            /* Alternative name format */
         url('fonts/open-sans.woff2') format('woff2'),
         url('fonts/open-sans.woff') format('woff');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}
Warning: Be cautious with local(). Some privacy-focused browsers and browser extensions block local() to prevent font fingerprinting -- a technique where websites detect which fonts are installed on your system to create a unique identifier. If privacy is a major concern for your audience, you might want to skip local() and always serve your own font files. However, for most websites, the performance benefit of local() outweighs this concern.

Web Font Formats

Over the years, several font file formats have been developed for the web. Understanding them helps you write efficient @font-face rules and support the widest range of browsers:

  • WOFF2 (Web Open Font Format 2.0) -- The modern standard. WOFF2 uses Brotli compression, resulting in files that are 30-50% smaller than WOFF. It is supported by all modern browsers (Chrome, Firefox, Safari, Edge, Opera) and should be your primary format. If you can only serve one format, make it WOFF2.
  • WOFF (Web Open Font Format 1.0) -- The predecessor to WOFF2. Uses zlib compression. Slightly larger files than WOFF2 but still much smaller than raw font formats. Has broader legacy support and serves as a good fallback for WOFF2.
  • TTF/OTF (TrueType Font / OpenType Font) -- Desktop font formats that can also be used on the web. They are uncompressed and therefore significantly larger than WOFF/WOFF2. Only use these if you need to support very old browsers or if you are converting them to WOFF2 yourself.
  • EOT (Embedded OpenType) -- A proprietary Microsoft format that was only supported by Internet Explorer. Since IE has been discontinued, EOT is no longer necessary for any modern project. You can safely ignore it.
  • SVG fonts -- An older format where fonts were defined using SVG markup. It was only used by very old versions of iOS Safari. It is completely obsolete today.

Example: Modern Font Format Stack

/* Modern approach -- WOFF2 only (covers 97%+ of browsers) */
@font-face {
    font-family: 'Inter';
    src: url('fonts/inter.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

/* Conservative approach -- WOFF2 with WOFF fallback */
@font-face {
    font-family: 'Inter';
    src: url('fonts/inter.woff2') format('woff2'),
         url('fonts/inter.woff') format('woff');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
}

/* Legacy "bulletproof" syntax (rarely needed today) */
@font-face {
    font-family: 'Inter';
    src: url('fonts/inter.eot');  /* IE9 compatibility mode */
    src: url('fonts/inter.eot?#iefix') format('embedded-opentype'),
         url('fonts/inter.woff2') format('woff2'),
         url('fonts/inter.woff') format('woff'),
         url('fonts/inter.ttf') format('truetype');
    font-weight: 400;
    font-style: normal;
}
Tip: For any new project in 2024 and beyond, just use WOFF2. Browser support is excellent (over 97% globally), and the file sizes are dramatically smaller. You can use tools like woff2_compress (command line), Font Squirrel's Webfont Generator, or Transfonter.org to convert other formats to WOFF2.

Using Google Fonts

Google Fonts is a free, open-source library of over 1,500 font families. It is the most popular way to add custom fonts to a website because it requires no font file management -- Google hosts and serves the files from their CDN. There are two primary methods to integrate Google Fonts.

Method 1: The Link Method (Recommended)

Add a <link> element in your HTML <head>. This is the recommended approach because it allows the browser to start downloading fonts in parallel with your CSS.

Example: Google Fonts via Link Tag

<!-- In your HTML <head> -->

<!-- Single font -->
<link rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Roboto&display=swap">

<!-- Multiple weights -->
<link rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap">

<!-- Multiple fonts with specific weights -->
<link rel="stylesheet"
      href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&family=Open+Sans:ital,wght@0,400;0,700;1,400&display=swap">

<!-- Then use in CSS -->
<style>
body {
    font-family: 'Roboto', sans-serif;
}
</style>

Method 2: The @import Method

Add an @import statement at the top of your CSS file. This is simpler when you want everything in CSS, but it has a performance disadvantage: the browser must first download your CSS file, find the @import, and only then begin downloading the font CSS -- creating a chain of sequential requests.

Example: Google Fonts via @import

/* At the top of your CSS file */
@import url('https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;700&display=swap');

/* Then use it normally */
h1, h2, h3 {
    font-family: 'Playfair Display', Georgia, serif;
}
Warning: The @import method creates a render-blocking chain: your HTML downloads the CSS, and then the CSS triggers another network request for the Google Fonts CSS. This adds a sequential delay. The <link> method in HTML is faster because the browser discovers the font request while parsing HTML, before it even starts downloading your main stylesheet. For production sites, always prefer the link method.

Optimizing Google Fonts with Preconnect

When you use Google Fonts, the browser needs to connect to two different domains: fonts.googleapis.com (for the CSS) and fonts.gstatic.com (for the actual font files). You can speed up font loading by adding <link rel="preconnect"> tags to establish these connections early.

Example: Preconnect for Google Fonts

<head>
    <!-- Preconnect to Google Fonts servers -->
    <link rel="preconnect" href="https://fonts.googleapis.com">
    <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>

    <!-- Then load the fonts -->
    <link rel="stylesheet"
          href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
</head>
Note: The crossorigin attribute on the gstatic.com preconnect is important. Font files are always requested with CORS (Cross-Origin Resource Sharing), so the preconnect must match. Without crossorigin, the browser would establish a non-CORS connection that cannot be reused for the font download, wasting the preconnect entirely.

FOIT and FOUT

Two common problems occur when loading web fonts:

  • FOIT (Flash of Invisible Text) -- The browser hides text while the web font is loading. The user sees blank space where the text should be. This is the default behavior in many browsers and creates a poor user experience because the page appears to load slowly even if the content is ready.
  • FOUT (Flash of Unstyled Text) -- The browser displays text in a fallback font while the web font is loading, then swaps to the web font when it arrives. The user sees a flash as the text changes appearance. This is generally considered the better approach because at least the content is readable immediately.

The font-display property (covered in the previous lesson) is the primary tool for controlling this behavior. Using font-display: swap creates FOUT, which is preferable because content is immediately accessible.

Minimizing FOUT with Font Matching

To reduce the visual jarring of FOUT, you can carefully choose fallback fonts that closely match the metrics (size, weight, spacing) of your web font. The CSS size-adjust, ascent-override, descent-override, and line-gap-override descriptors in @font-face let you fine-tune fallback fonts to minimize layout shifts when the web font loads.

Example: Reducing Layout Shift with Fallback Font Adjustments

/* Define an adjusted fallback font */
@font-face {
    font-family: 'Inter Fallback';
    src: local('Arial');
    size-adjust: 107%;
    ascent-override: 90%;
    descent-override: 22%;
    line-gap-override: 0%;
}

/* Define the real web font */
@font-face {
    font-family: 'Inter';
    src: url('fonts/inter.woff2') format('woff2');
    font-weight: 100 900;
    font-display: swap;
}

/* Use both in your font stack */
body {
    font-family: 'Inter', 'Inter Fallback', sans-serif;
}

Preloading Fonts

For critical fonts that must load as quickly as possible, you can use <link rel="preload"> to tell the browser to start downloading the font file immediately, even before it encounters the @font-face rule in your CSS. This is especially valuable when font files are referenced deep in your CSS or when using self-hosted fonts.

Example: Preloading Font Files

<head>
    <!-- Preload the most important font files -->
    <link rel="preload"
          href="/fonts/inter-regular.woff2"
          as="font"
          type="font/woff2"
          crossorigin>

    <link rel="preload"
          href="/fonts/inter-bold.woff2"
          as="font"
          type="font/woff2"
          crossorigin>

    <!-- Your stylesheet that contains @font-face rules -->
    <link rel="stylesheet" href="/css/styles.css">
</head>
Warning: Only preload fonts that are actually used on the current page. Every preloaded resource competes for bandwidth with other critical resources like HTML, CSS, and JavaScript. Preloading fonts you do not need wastes bandwidth and can actually slow down the page. A good rule of thumb is to preload at most 1-2 font files -- typically the regular weight and bold weight of your primary body font.

The unicode-range Descriptor

The unicode-range descriptor limits which Unicode characters a font file covers. The browser will only download the font file if the page contains at least one character in the specified range. This is incredibly powerful for performance because you can split a large font into smaller subsets.

Example: Unicode Range Subsetting

/* Latin characters only */
@font-face {
    font-family: 'Noto Sans';
    src: url('fonts/noto-sans-latin.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC,
                   U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074,
                   U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,
                   U+FEFF, U+FFFD;
}

/* Arabic characters */
@font-face {
    font-family: 'Noto Sans';
    src: url('fonts/noto-sans-arabic.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011,
                   U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC;
}

/* Cyrillic characters */
@font-face {
    font-family: 'Noto Sans';
    src: url('fonts/noto-sans-cyrillic.woff2') format('woff2');
    font-weight: 400;
    font-style: normal;
    font-display: swap;
    unicode-range: U+0400-045F, U+0490-0491, U+04B0-04B1, U+2116;
}

/* The browser only downloads the subset files it needs! */
body {
    font-family: 'Noto Sans', sans-serif;
}
Tip: Google Fonts automatically uses unicode-range subsetting. When you request a Google Font, the CSS it generates contains multiple @font-face rules, each covering a different script (Latin, Latin Extended, Cyrillic, Greek, etc.). The browser only downloads the subsets it actually needs for the text on your page. This is one reason Google Fonts performs well even for fonts with thousands of glyphs.

Subsetting Fonts for Performance

Font subsetting means creating a smaller version of a font file that only contains the characters you actually need. A full font file might contain thousands of glyphs covering Latin, Cyrillic, Greek, Arabic, CJK, and many other scripts. If your website is only in English, you are wasting bandwidth on characters you will never use.

There are several approaches to subsetting:

  • Unicode range subsetting in CSS -- Use the unicode-range descriptor as shown above to split fonts into per-script files. The browser downloads only the files it needs.
  • Static subsetting with tools -- Use tools like pyftsubset (from fonttools), glyphhanger, or Font Squirrel's Webfont Generator to create font files that only contain the characters you need.
  • Content-based subsetting -- Analyze the actual text content of your website and create a font subset that includes only those specific characters. This produces the smallest possible font files.

Example: Using pyftsubset to Create a Subset

# Install fonttools (Python)
pip install fonttools brotli

# Create a Latin-only subset
pyftsubset MyFont.ttf \
    --output-file=MyFont-latin.woff2 \
    --flavor=woff2 \
    --layout-features='*' \
    --unicodes=U+0000-00FF

# Create a subset with only specific characters
pyftsubset MyFont.ttf \
    --output-file=MyFont-minimal.woff2 \
    --flavor=woff2 \
    --text="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,!?- "

# Using glyphhanger to analyze a page and subset
npx glyphhanger https://example.com --subset=MyFont.ttf --formats=woff2

Variable Fonts

Variable fonts are one of the most significant advances in web typography. A traditional font family requires separate files for each combination of weight and style -- Regular, Bold, Italic, Bold Italic, Light, Light Italic, and so on. A full family might require 8, 12, or even 18 separate files. A variable font packs all of these variations -- and infinite values in between -- into a single file.

How Variable Fonts Work

Variable fonts use axes of variation. Each axis represents a property that can be continuously adjusted along a range. The font file contains master designs at the extremes, and the browser interpolates between them to produce any value in between. For example, instead of only having weights 400 and 700, a variable font might offer any weight from 100 to 900 in increments as small as a single unit.

Registered Axes

The OpenType specification defines five registered (standard) axes, each identified by a four-letter lowercase tag:

  • wght (Weight) -- Controls the thickness of strokes. Range varies by font, commonly 100-900. Maps to the font-weight CSS property.
  • wdth (Width) -- Controls how condensed or extended the characters are. Expressed as a percentage of normal (100%). Maps to the font-stretch CSS property.
  • ital (Italic) -- Switches between roman (0) and italic (1) designs. Unlike a simple slant, italic designs typically feature different letterforms. Maps to font-style: italic.
  • slnt (Slant) -- Controls the angle of the text, typically from 0 (upright) to -12 or -20 degrees. Unlike the ital axis, slant does not change letterforms -- it just tilts them. Maps to font-style: oblique <angle>.
  • opsz (Optical Size) -- Adjusts the font's design for different sizes. At small sizes, letters get slightly wider with more open counters and thicker thin strokes for legibility. At large sizes, the design becomes more refined with higher contrast. Maps to the font-optical-sizing CSS property.

Using Variable Fonts with Standard CSS Properties

For registered axes, you can use standard CSS properties instead of the lower-level font-variation-settings:

Example: Variable Fonts with Standard CSS Properties

/* Load a variable font */
@font-face {
    font-family: 'Inter';
    src: url('fonts/Inter-Variable.woff2') format('woff2-variations');
    font-weight: 100 900;  /* Declare the full weight range */
    font-style: normal;
    font-display: swap;
}

/* Use any weight in the range -- not just 100, 200, 300, etc. */
.thin { font-weight: 100; }
.extra-light { font-weight: 200; }
.light { font-weight: 300; }
.regular { font-weight: 400; }
.medium { font-weight: 500; }
.semi-bold { font-weight: 600; }
.bold { font-weight: 700; }
.extra-bold { font-weight: 800; }
.black { font-weight: 900; }

/* You can even use in-between values! */
.custom-450 { font-weight: 450; }
.custom-550 { font-weight: 550; }
.custom-650 { font-weight: 650; }

/* Variable font-stretch for width axis */
.condensed { font-stretch: 75%; }
.normal-width { font-stretch: 100%; }
.expanded { font-stretch: 125%; }

/* Optical sizing (auto by default) */
body {
    font-optical-sizing: auto;
}

The font-variation-settings Property

The font-variation-settings property gives you direct access to all axes of a variable font, including both registered and custom axes. Each axis is specified as a pair of a four-character tag string and a numeric value.

Example: font-variation-settings

/* Set weight and width axes directly */
.custom-variation {
    font-variation-settings: 'wght' 625, 'wdth' 85;
}

/* Italic axis */
.partial-italic {
    font-variation-settings: 'ital' 0.5;  /* Half italic! */
}

/* Slant axis */
.slanted {
    font-variation-settings: 'slnt' -8;  /* 8 degrees of slant */
}

/* Optical size axis (manual override) */
.caption-text {
    font-size: 12px;
    font-variation-settings: 'opsz' 12;
}
.display-text {
    font-size: 72px;
    font-variation-settings: 'opsz' 72;
}

/* Combining multiple axes */
.hero-heading {
    font-variation-settings: 'wght' 800, 'wdth' 110, 'opsz' 48;
}
Warning: Prefer standard CSS properties (font-weight, font-stretch, font-style, font-optical-sizing) over font-variation-settings whenever possible. The font-variation-settings property does not inherit axis values independently -- setting any axis resets all axes that are not explicitly specified. Additionally, animations and transitions work better with standard properties. Reserve font-variation-settings for custom axes or for values that cannot be expressed through standard properties.

Custom Axes

Beyond the five registered axes, font designers can create custom axes identified by four uppercase letters. These are unique to each font and offer creative possibilities that go far beyond weight and width:

Example: Custom Variable Font Axes

/* Recursive font -- has a MONO axis (monospace amount)
   and CASL axis (casual style) */
@font-face {
    font-family: 'Recursive';
    src: url('fonts/Recursive-Variable.woff2') format('woff2-variations');
    font-weight: 300 1000;
    font-display: swap;
}

/* Switch between proportional and monospace */
.proportional {
    font-variation-settings: 'MONO' 0, 'CASL' 0;
}
.monospace {
    font-variation-settings: 'MONO' 1, 'CASL' 0;
}

/* Use the casual style axis */
.casual {
    font-variation-settings: 'MONO' 0, 'CASL' 1;
}

/* Combine for a casual monospace look */
.casual-mono {
    font-variation-settings: 'MONO' 1, 'CASL' 1;
}

Variable Fonts: Performance Benefits

Variable fonts offer significant performance advantages over traditional static fonts:

  • Fewer HTTP requests -- Instead of downloading 4-8 separate font files (regular, bold, italic, bold-italic, light, medium, etc.), you download just one or two variable font files.
  • Smaller total file size -- A single variable font file is usually smaller than the combined size of the equivalent static fonts. For example, a variable font covering weights 100-900 might be 150KB, while nine separate static fonts at 25KB each would total 225KB.
  • Design flexibility -- You can use any weight, width, or optical size without adding extra file downloads. Need font-weight: 550? No problem -- no additional file needed.

Animating Variable Fonts

One of the most exciting features of variable fonts is the ability to animate between axis values. Because the browser interpolates between designs in real-time, you can create smooth transitions between different weights, widths, or custom axes.

Example: Animating Variable Font Properties

/* Smooth weight transition on hover */
.animated-weight {
    font-weight: 400;
    transition: font-weight 0.3s ease;
}

.animated-weight:hover {
    font-weight: 700;
}

/* Keyframe animation cycling through weights */
@keyframes breathe {
    0%, 100% {
        font-variation-settings: 'wght' 300;
    }
    50% {
        font-variation-settings: 'wght' 700;
    }
}

.breathing-text {
    animation: breathe 3s ease-in-out infinite;
}

/* Animating multiple axes simultaneously */
@keyframes morph {
    0% {
        font-variation-settings: 'wght' 300, 'wdth' 75;
    }
    50% {
        font-variation-settings: 'wght' 800, 'wdth' 125;
    }
    100% {
        font-variation-settings: 'wght' 300, 'wdth' 75;
    }
}

.morphing-text {
    animation: morph 4s ease-in-out infinite;
}
Tip: Variable font animations are GPU-friendly and perform well even on mobile devices because the browser only needs to interpolate between pre-defined masters. However, use font animations sparingly -- they can be distracting and may cause performance issues if applied to large blocks of text. They work best for headings, logos, or interactive UI elements.

Self-Hosting vs. CDN: Making the Right Choice

You have two main options for serving web fonts: using a CDN like Google Fonts, or self-hosting the font files on your own server. Each approach has trade-offs:

  • CDN (Google Fonts, Adobe Fonts, etc.) -- Easier setup, automatic format selection, and unicode-range subsetting. However, it introduces a third-party dependency, requires extra DNS lookups and connections, and raises GDPR/privacy concerns in some regions because user IP addresses are sent to Google's servers.
  • Self-hosting -- Full control over caching, no third-party dependency, better privacy compliance, and potentially faster loading (fewer DNS lookups). However, you need to manage font files, handle subsetting yourself, and serve appropriate formats.

For production websites, especially those with European audiences subject to GDPR, self-hosting fonts is increasingly recommended. Google Fonts can still be used during development for convenience, but the font files should be downloaded and self-hosted for production.

Complete Web Font Optimization Checklist

Here is a comprehensive checklist for optimizing web fonts on any project:

  1. Use WOFF2 format -- It offers the best compression and broadest modern browser support.
  2. Subset your fonts -- Remove characters you do not need. Use unicode-range or tools like pyftsubset.
  3. Limit font families -- Use at most 2-3 font families. Each one adds weight and complexity.
  4. Limit weights and styles -- Only load the weights and styles you actually use. Do not load 8 weights if you only use 3.
  5. Consider variable fonts -- If you need 3 or more weights from the same family, a variable font is likely smaller.
  6. Set font-display: swap -- Ensure text is visible immediately while fonts load.
  7. Preload critical fonts -- Use <link rel="preload"> for the 1-2 most important font files.
  8. Use preconnect -- If using a font CDN, add preconnect hints for both the CSS and font file domains.
  9. Self-host when possible -- Eliminates third-party dependencies and extra DNS lookups.
  10. Match fallback metrics -- Use size-adjust and override descriptors to minimize layout shift during FOUT.

Exercise 1: Self-Host a Google Font

Choose any Google Font with at least 3 weights (for example, Inter with weights 400, 500, and 700). Download the WOFF2 files from Google Fonts (you can use the google-webfonts-helper tool at gwfh.mranftl.com). Create @font-face rules for all three weights, including the local() function, WOFF2 format, font-display: swap, and appropriate unicode-range for Latin characters. Then apply the font to a sample HTML page with headings, body text, and bold text. Compare the network requests in your browser's DevTools with the Google Fonts CDN approach -- how many requests are made in each case? What is the total file size difference?

Exercise 2: Implement a Variable Font with Animations

Download the "Recursive" variable font (available on Google Fonts). Create a single @font-face rule that loads the variable font file with a weight range of 300 to 1000. Build a demo page with the following features: (1) A heading that smoothly transitions from weight 300 to weight 900 on hover using CSS transitions. (2) A paragraph where the weight can be controlled by a CSS custom property, demonstrating how a single file replaces many static files. (3) A "breathing" animation that uses keyframes to cycle the weight between 400 and 700 over 3 seconds. (4) If the font supports the CASL (casual) axis, add a toggle that switches between casual and normal styles. Document the total file size of the variable font versus the equivalent static font files for 3-4 weights.

Exercise 3: Font Loading Performance Audit

Pick any live website that uses web fonts (you can inspect this using browser DevTools under the Network tab, filtered by Font). For the chosen website, answer the following questions: (1) What font formats are being served? Are they using WOFF2? (2) How many font files are downloaded? Could a variable font reduce this number? (3) Is font-display being used? What value? (4) Are fonts preloaded with <link rel="preload">? (5) Is preconnect used for external font domains? (6) Is unicode-range being used for subsetting? (7) What is the total font payload size? Then write a brief report recommending specific optimizations with estimated performance improvements.

ES
Edrees Salih
18 hours ago

We are still cooking the magic in the way!