Web Fonts - Custom Typography Beyond System Fonts

What You'll Learn

  • Define custom fonts with @font-face for brand-consistent typography
  • Understand font formats (WOFF2, WOFF, TTF) and which to use for modern browsers
  • Control loading behavior with font-display to avoid invisible text
  • Use Google Fonts CDN for quick, easy access to hundreds of free fonts
  • Self-host fonts for better privacy, performance, and GDPR compliance
  • Optimize loading with preconnect and preload resource hints
  • Reduce file sizes with font subsetting and unicode-range
  • Follow best practices to limit fonts, weights, and total page weight
  • Choose between CDN and self-hosted fonts based on project requirements

Introduction to Web Fonts

Web fonts allow you to use custom typefaces beyond the limited set of system fonts installed on users' computers. Instead of being restricted to Arial, Times New Roman, and a handful of others, you can use thousands of professional fonts to create distinctive, brand-consistent typography.

Before web fonts (pre-2010), designers had to use images or Flash for custom typography. Today, with @font-face and services like Google Fonts, using custom fonts is simple, fast, and universally supported across all modern browsers.

@font-face Rule

The @font-face rule defines a custom font that browsers can download and use. You specify the font file location, the name to reference it by, and various properties like weight and style.

/* Basic @font-face */ @font-face { font-family: 'MyCustomFont'; /* Name for CSS */ src: url('fonts/mycustomfont.woff2') format('woff2'), url('fonts/mycustomfont.woff') format('woff'); font-weight: 400; font-style: normal; font-display: swap; /* Loading behavior */ } /* Use the font */ body { font-family: 'MyCustomFont', sans-serif; } /* Multiple weights require separate @font-face rules */ @font-face { font-family: 'MyCustomFont'; src: url('fonts/mycustomfont-bold.woff2') format('woff2'); font-weight: 700; /* Different weight, same family name */ font-style: normal; font-display: swap; }

Font Formats

Fonts come in various formats with different compression levels and browser support. For modern web development, you only need WOFF2 and optionally WOFF as a fallback.

/* Modern (2020+): WOFF2 + WOFF fallback */ @font-face { font-family: 'Roboto'; src: url('fonts/roboto.woff2') format('woff2'), /* 98%+ browsers */ url('fonts/roboto.woff') format('woff'); /* IE 11 fallback */ font-weight: 400; font-style: normal; font-display: swap; } /* Legacy (pre-2020): Multiple formats - NO LONGER NEEDED */ @font-face { font-family: 'OldFont'; src: url('fonts/old.eot'); /* IE 9 */ src: url('fonts/old.eot?#iefix') format('embedded-opentype'), url('fonts/old.woff2') format('woff2'), url('fonts/old.woff') format('woff'), url('fonts/old.ttf') format('truetype'), url('fonts/old.svg#OldFont') format('svg'); /* DON'T DO THIS - unnecessary complexity */ }

font-display - Controlling Load Behavior

The font-display property controls how text is displayed while fonts are loading. This is crucial for performance and user experience.

/* Recommended: swap - show fallback immediately */ @font-face { font-family: 'Roboto'; src: url('roboto.woff2') format('woff2'); font-display: swap; /* Best for body text */ } /* Values explained */ font-display: swap; /* Show fallback immediately, swap when loaded */ font-display: block; /* Hide text ~3s, then show font (FOIT) */ font-display: fallback; /* Brief block, swap if quick, else stick with fallback */ font-display: optional; /* Use font only if already cached */ font-display: auto; /* Browser decides (usually like block) */ /* Use cases */ @font-face { font-family: 'BodyFont'; src: url('body.woff2') format('woff2'); font-display: swap; /* Body text: always show text */ } @font-face { font-family: 'IconFont'; src: url('icons.woff2') format('woff2'); font-display: block; /* Icons: wait for font to avoid showing wrong symbols */ } @font-face { font-family: 'DecorativeFont'; src: url('decorative.woff2') format('woff2'); font-display: optional; /* Non-critical: only if fast */ }

Google Fonts - Quick & Easy

Google Fonts is the easiest way to add web fonts. It's free, hosts fonts on a fast global CDN, and provides automatic optimization and subsetting.

<!-- 1. Add preconnect for faster loading --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <!-- 2. Load the font with display=swap --> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&display=swap" rel="stylesheet"> <!-- Multiple fonts separated by & --> <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@400;700&family=Playfair+Display:wght@700&display=swap" rel="stylesheet"> /* Use in CSS */ body { font-family: 'Roboto', sans-serif; } h1, h2, h3 { font-family: 'Playfair Display', serif; }

Self-Hosting Fonts

Self-hosting means downloading font files and serving them from your own server. This provides better privacy, performance, and control compared to using a CDN.

/* Directory structure */ /* project/ ├── fonts/ │ ├── roboto-regular.woff2 │ ├── roboto-bold.woff2 │ └── roboto-italic.woff2 └── css/ └── styles.css */ /* Define fonts in CSS */ @font-face { font-family: 'Roboto'; src: url('../fonts/roboto-regular.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; } @font-face { font-family: 'Roboto'; src: url('../fonts/roboto-bold.woff2') format('woff2'); font-weight: 700; font-style: normal; font-display: swap; } @font-face { font-family: 'Roboto'; src: url('../fonts/roboto-italic.woff2') format('woff2'); font-weight: 400; font-style: italic; font-display: swap; } /* Use it */ body { font-family: 'Roboto', sans-serif; }

CDN vs Self-Hosted Comparison

Both approaches have trade-offs. Choose based on your priorities: ease of use vs privacy, or prototyping speed vs production performance.

Google Fonts (CDN) ✓ Easy setup (one <link> tag) ✓ Automatic optimization ✓ Fast global CDN ✓ No bandwidth cost ✗ Privacy concerns (Google tracking) ✗ Third-party dependency ✗ Extra DNS lookup ✗ GDPR compliance issues Self-Hosted Fonts ✓ Better privacy (no tracking) ✓ No external dependencies ✓ Works offline ✓ Full caching control ✓ GDPR compliant ✗ More setup work ✗ Manual updates needed ✗ Uses your bandwidth

Font Loading Strategies

Optimize font loading with resource hints (preconnect, preload) and the Font Loading API for fine-grained control.

<!-- 1. Preconnect (for CDN fonts like Google Fonts) --> <link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <!-- Saves 100-300ms by resolving DNS early --> <!-- 2. Preload (for critical self-hosted fonts) --> <link rel="preload" href="/fonts/roboto-regular.woff2" as="font" type="font/woff2" crossorigin> <!-- Starts download immediately, before CSS is parsed --> <!-- Only preload 1-2 critical fonts max! --> // 3. Font Loading API (for precise control) const font = new FontFace( 'Roboto', 'url(/fonts/roboto-regular.woff2) format("woff2")', { weight: '400', style: 'normal' } ); font.load().then((loadedFont) => { document.fonts.add(loadedFont); document.body.style.fontFamily = 'Roboto, sans-serif'; console.log('Font loaded!'); }).catch((error) => { console.error('Font failed to load', error); });

Font Subsetting

Subsetting reduces font file size by including only the characters you need. This can shrink files by 50-90%, dramatically improving load times.

<!-- Google Fonts subsetting --> <!-- Latin-only (default): ~40KB --> <link href="https://fonts.googleapis.com/css2?family=Roboto&subset=latin"> <!-- Specific characters only: ~5-10KB --> <link href="https://fonts.googleapis.com/css2?family=Roboto&text=Hello%20World"> <!-- Multiple subsets --> <link href="https://fonts.googleapis.com/css2?family=Roboto&subset=latin,latin-ext,cyrillic"> /* Manual subsetting with unicode-range */ @font-face { font-family: 'Roboto'; src: url('roboto-latin.woff2') format('woff2'); unicode-range: U+0000-00FF, U+0131, U+0152-0153; /* Only Latin characters */ } @font-face { font-family: 'Roboto'; src: url('roboto-latin-ext.woff2') format('woff2'); unicode-range: U+0100-024F, U+0259, U+1E00-1EFF; /* Extended Latin */ } /* Browser downloads only the subset it needs */

Key Takeaways

  • @font-face: Defines custom fonts with family name, source URL, weight, style, and display
  • Font formats: Use WOFF2 (best compression) + WOFF (IE 11 fallback); skip TTF, EOT, SVG
  • font-display: swap: Recommended for all fonts to avoid invisible text (FOIT)
  • Google Fonts: Easy CDN solution with automatic optimization, but has privacy concerns
  • Self-hosting: Better privacy, performance, and GDPR compliance, but requires more setup
  • Preconnect: Use for CDN fonts to save 100-300ms on DNS and connection
  • Preload: Only for 1-2 critical self-hosted fonts; too many hurts performance
  • Subsetting: Reduce file size 50-90% by including only needed characters
  • Limit fonts: Use 2-3 font families max, and only load weights you actually use
  • Each weight/style requires separate @font-face declaration and file
  • Font files are large (20-200KB each); 6 weights can add 600KB+
  • WOFF2 offers ~30% better compression than WOFF; ~70% better than TTF
  • Use unicode-range for automatic subset loading based on page content
  • Variable fonts (next lesson) can replace multiple weight files with a single file
  • Always provide fallback system fonts in font-family stack
  • Tools: google-webfonts-helper, Fontsource, Transfonter, Font Squirrel, pyftsubset