Typography is the single most influential design decision on a webpage. Approximately 95% of the web is text, and how that text looks โ its typeface, size, weight, and spacing โ determines whether a site feels professional, playful, trustworthy, or dated. In this lesson you will master the CSS font properties and learn to load custom web fonts with @font-face and Google Fonts โ the foundation of all CSS typography work.
Core Font Properties
| Property | Controls | Common Values |
|---|---|---|
font-family |
Typeface โ with fallback stack | 'Inter', system-ui, sans-serif |
font-size |
Text size โ relative or absolute | 1rem, 16px, clamp(1rem, 2.5vw, 1.5rem) |
font-weight |
Stroke thickness | 400 (normal), 700 (bold), 100โ900 |
font-style |
Upright vs slanted | normal, italic, oblique |
font-variant |
Small caps and OpenType features | small-caps, normal |
font |
Shorthand for all font properties | 700 1.25rem/1.4 'Inter', sans-serif |
Font Size Units Compared
| Unit | Relative To | Best For |
|---|---|---|
px |
Absolute pixels | Borders, shadows โ not recommended for font-size |
rem |
Root element (html) font-size |
Body text, headings โ scales with user browser settings |
em |
Parent element font-size | Component-relative sizing โ padding, margin inside components |
% |
Parent element font-size | Similar to em โ less common for font-size |
vw |
Viewport width | Fluid headings โ usually combined with clamp() |
clamp(min, val, max) |
Fluid between min and max | Responsive type that scales between two size limits |
Loading Web Fonts
| Method | How | Pros / Cons |
|---|---|---|
| Google Fonts (link) | <link> in HTML head |
Simplest; CDN-hosted; privacy considerations |
| Google Fonts (CSS import) | @import url(...) in CSS |
Easy but adds a render-blocking request |
@font-face |
Self-host font files | Full control; best performance; GDPR-friendly |
| System font stack | No loading โ use OS fonts | Instant; no FOUT; less branding control |
font-family as a stack with multiple fallbacks: 'Inter', system-ui, -apple-system, sans-serif. If Inter fails to load (network error, ad-blocker), the browser moves to the next font in the stack. Without fallbacks, users see the browser’s default serif font โ usually Times New Roman.html using a percentage โ html { font-size: 100%; } โ never a fixed pixel value. This respects the user’s browser font-size preference. Then use rem throughout your stylesheet for all text sizes. If a user has set their browser to 20px base, all your rem values scale up proportionally.weights=400;700.Basic Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Font Properties and Web Fonts</title>
<!-- Google Fonts: two weights only -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&family=Playfair+Display:ital,wght@0,700;1,700&display=swap" rel="stylesheet">
<style>
/* Base โ respects browser font-size preference */
html { font-size: 100%; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: 'Inter', system-ui, -apple-system, sans-serif;
font-size: 1rem; /* 16px at browser default */
font-weight: 400;
line-height: 1.6;
color: #1e293b;
background: #f8fafc;
padding: 40px 24px;
max-width: 680px;
margin: 0 auto;
}
/* Display heading โ Playfair Display, bold */
.display-heading {
font-family: 'Playfair Display', Georgia, serif;
font-size: clamp(2rem, 5vw, 3.5rem); /* fluid: 32pxโ56px */
font-weight: 700;
line-height: 1.1;
letter-spacing: -0.02em;
color: #0f172a;
margin-bottom: 16px;
}
/* Italic display variant */
.display-heading em {
font-style: italic;
color: #4f46e5;
}
/* Section heading โ Inter, semibold */
.section-heading {
font-family: 'Inter', system-ui, sans-serif;
font-size: 1.25rem; /* 20px */
font-weight: 700;
letter-spacing: -0.01em;
color: #0f172a;
margin: 32px 0 12px;
}
/* Body paragraph */
p {
font-size: 1rem;
line-height: 1.75;
color: #475569;
margin-bottom: 16px;
}
/* Small label / caption */
.label {
font-size: 0.75rem; /* 12px */
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.08em;
color: #64748b;
}
/* Weight demo */
.weight-demo { display: flex; gap: 24px; flex-wrap: wrap; margin-bottom: 24px; }
.weight-item { font-size: 1rem; color: #0f172a; }
/* @font-face self-hosting pattern (shown as code) */
.code-block {
background: #1e293b;
color: #e2e8f0;
padding: 20px 24px;
border-radius: 10px;
font-family: 'Courier New', monospace;
font-size: 0.825rem;
line-height: 1.7;
overflow-x: auto;
white-space: pre;
margin-bottom: 24px;
}
/* font shorthand demo */
.shorthand-demo {
/* weight size/line-height family */
font: 700 1.125rem/1.4 'Playfair Display', serif;
color: #0f172a;
margin-bottom: 8px;
}
</style>
</head>
<body>
<p class="label">Display Heading โ Playfair Display + clamp()</p>
<h1 class="display-heading">Beautiful <em>Typography</em> Starts Here</h1>
<p>This body text uses Inter at 1rem (16px) with a line-height of 1.75 โ comfortable for reading long-form content. The colour is a dark slate rather than pure black, which reduces eye strain.</p>
<h2 class="section-heading">Font Weight Scale</h2>
<div class="weight-demo">
<span class="weight-item" style="font-weight:300">300 Light</span>
<span class="weight-item" style="font-weight:400">400 Regular</span>
<span class="weight-item" style="font-weight:500">500 Medium</span>
<span class="weight-item" style="font-weight:600">600 SemiBold</span>
<span class="weight-item" style="font-weight:700">700 Bold</span>
<span class="weight-item" style="font-weight:800">800 ExtraBold</span>
</div>
<h2 class="section-heading">Self-Hosting with @font-face</h2>
<div class="code-block">@font-face {
font-family: 'Inter';
src: url('/fonts/inter-400.woff2') format('woff2');
font-weight: 400;
font-style: normal;
font-display: swap; /* show fallback while loading */
}
@font-face {
font-family: 'Inter';
src: url('/fonts/inter-700.woff2') format('woff2');
font-weight: 700;
font-style: normal;
font-display: swap;
}</div>
<h2 class="section-heading">font Shorthand</h2>
<p class="shorthand-demo">font: 700 1.125rem/1.4 'Playfair Display', serif</p>
<p style="font-size:0.875rem;color:#64748b">Shorthand order: style? variant? weight? size/line-height family</p>
</body>
</html>
How It Works
Step 1 โ html font-size: 100% Enables User Scaling
Setting font-size: 100% on html means the base size equals whatever the user has set in their browser preferences (typically 16px). If they set 20px, all rem values scale proportionally. Setting html { font-size: 16px } would override and ignore this preference โ an accessibility issue.
Step 2 โ clamp() Makes Headings Fluid
clamp(2rem, 5vw, 3.5rem) creates a heading that is at least 32px, scales with the viewport (5% of viewport width), and never exceeds 56px. On a 320px phone: 5vw = 16px, but clamp holds it at 32px min. On a 1400px monitor: 5vw = 70px, but clamp caps it at 56px.
Step 3 โ The Font Stack Provides Fallbacks
If Inter fails to load, system-ui selects the OS’s default UI font (San Francisco on macOS, Segoe UI on Windows). -apple-system provides an older Safari fallback. sans-serif is the final generic fallback that every browser must support.
Step 4 โ @font-face Gives Full Control
Self-hosting with @font-face keeps font requests on your own domain, eliminating third-party GDPR concerns. font-display: swap tells the browser to show the fallback font immediately and swap to the web font when it loads โ preventing invisible text during load.
Step 5 โ font Shorthand Consolidates Declarations
The font shorthand must include at minimum font-size and font-family โ omitting either invalidates the entire rule. Optional values before size: font-style, font-variant, font-weight. Line-height follows size separated by a slash: 1rem/1.6.
Real-World Example: Design System Type Scale
/* type-scale.css */
:root {
/* Base */
--font-sans: 'Inter', system-ui, -apple-system, sans-serif;
--font-serif: 'Playfair Display', Georgia, serif;
--font-mono: 'JetBrains Mono', 'Courier New', monospace;
/* Type scale โ Major Third (1.25x) */
--text-xs: 0.64rem; /* ~10px */
--text-sm: 0.8rem; /* ~13px */
--text-base: 1rem; /* 16px */
--text-lg: 1.25rem; /* 20px */
--text-xl: 1.563rem; /* 25px */
--text-2xl: 1.953rem; /* 31px */
--text-3xl: 2.441rem; /* 39px */
--text-4xl: 3.052rem; /* 49px */
/* Weights */
--weight-normal: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
--weight-extrabold:800;
/* Line heights */
--leading-tight: 1.2;
--leading-snug: 1.375;
--leading-normal: 1.5;
--leading-relaxed:1.625;
--leading-loose: 1.75;
}
/* Utility classes */
.text-xs { font-size: var(--text-xs); }
.text-sm { font-size: var(--text-sm); }
.text-base { font-size: var(--text-base); }
.text-lg { font-size: var(--text-lg); }
.text-xl { font-size: var(--text-xl); }
.text-2xl { font-size: var(--text-2xl); }
.text-3xl { font-size: var(--text-3xl); }
.text-4xl { font-size: var(--text-4xl); }
.font-normal { font-weight: var(--weight-normal); }
.font-medium { font-weight: var(--weight-medium); }
.font-semibold { font-weight: var(--weight-semibold); }
.font-bold { font-weight: var(--weight-bold); }
/* Heading styles */
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-sans);
font-weight: var(--weight-bold);
line-height: var(--leading-tight);
color: #0f172a;
letter-spacing: -0.02em;
}
h1 { font-size: clamp(var(--text-2xl), 5vw, var(--text-4xl)); }
h2 { font-size: clamp(var(--text-xl), 3vw, var(--text-3xl)); }
h3 { font-size: var(--text-xl); }
h4 { font-size: var(--text-lg); }
p, li { font-size: var(--text-base); line-height: var(--leading-relaxed); }
small { font-size: var(--text-sm); }
code { font-family: var(--font-mono); font-size: 0.9em; }
Common Mistakes
Mistake 1 โ Setting html font-size in px
โ Wrong โ overrides the user’s accessibility font preference:
html { font-size: 16px; } /* ignores browser/user font-size setting */
โ Correct โ use percentage to respect user preferences:
html { font-size: 100%; } /* 100% of whatever the user has set */
Mistake 2 โ Loading all font weights
โ Wrong โ loads 9 weights, most unused โ heavy network cost:
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@100;200;300;400;500;600;700;800;900&display=swap" rel="stylesheet">
โ Correct โ load only what you actually use:
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap" rel="stylesheet">
Mistake 3 โ Using em for font-size on nested elements
โ Wrong โ em compounds: a 0.875em element inside another 0.875em element = 0.766em:
.card { font-size: 0.875em; }
.card .label { font-size: 0.875em; } /* compounds to ~12px โ unexpectedly small */
โ Correct โ use rem for font-size to always be relative to root:
.card { font-size: 0.875rem; }
.card .label { font-size: 0.75rem; } /* always relative to html โ predictable */
Quick Reference
| Property | Values | Notes |
|---|---|---|
font-family |
'Inter', system-ui, sans-serif |
Always include fallbacks |
font-size |
1rem, clamp(1rem, 3vw, 2rem) |
Use rem not px for accessibility |
font-weight |
400, 700, 100โ900 |
Numeric values โ no “bold” keyword in design systems |
font-style |
normal, italic |
Use semantic <em> for meaning, CSS for decoration |
font-display |
swap, block, fallback |
In @font-face โ swap prevents invisible text |
clamp(min, val, max) |
clamp(1rem, 4vw, 2.5rem) |
Fluid type โ scales with viewport |
@font-face |
Self-hosted woff2 files | Best performance and privacy |