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.
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.
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