Typography accounts for roughly 95% of web design. Nearly everything users interact with on the web is text — headlines, body copy, navigation labels, button text, form labels, error messages. Despite this, font choices and typographic details are often treated as an afterthought, addressed only after layout and functionality are in place. Strong typography transforms a mediocre page into a professional one. Weak typography undermines even the best visual design.
Modern web typography has evolved well beyond choosing between Arial and Times New Roman. Variable fonts, font loading strategies, fluid type scales, and advanced OpenType features give developers fine-grained control over how text renders across devices and screen sizes. This guide covers the fundamentals and advanced techniques that produce readable, performant, and visually refined text on the web.
Web Fonts: How We Got Here
Before @font-face gained cross-browser support around 2009-2010, web designers were limited to a handful of “web-safe” fonts: Arial, Verdana, Georgia, Times New Roman, Courier New, and a few others. Google Fonts launched in 2010 and opened thousands of typefaces for free web use. Adobe Fonts (formerly Typekit) offered premium typefaces through a subscription model. Suddenly, typographic quality on the web could match print.
/* Loading a web font with @font-face */
@font-face {
font-family: 'Space Grotesk';
src: url('/fonts/space-grotesk-var.woff2') format('woff2');
font-weight: 300 700;
font-display: swap;
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+2000-206F;
}
Choosing Fonts: Readability First
Font selection should always prioritize readability. A display font that looks striking in a hero section can become exhausting in body text. The best body text fonts share common characteristics: open counters (the enclosed spaces within letters like ‘o’, ‘e’, ‘a’), generous x-height (the height of lowercase letters), clear distinction between similar characters (‘I’, ‘l’, ‘1’ should look different), and consistent stroke width.
Recommended Body Text Fonts
- IBM Plex Sans — Clean, technical, excellent readability at small sizes. Designed by IBM for developer tools and documentation
- Inter — Designed specifically for screens. Open counters, tall x-height, excellent at small sizes. Free and variable
- Source Sans 3 — Adobe’s open-source sans-serif. Professional, neutral, highly readable
- Literata — A serif face optimized for long-form reading on screens. Used by Google Play Books
Recommended Heading Fonts
- Space Grotesk — Geometric sans-serif with personality. Distinctive without being distracting
- DM Serif Display — Elegant serif with strong contrast. Works beautifully for titles
- Fraunces — Variable font with a soft, old-style aesthetic. Wide range of weights and optical sizes
Font Pairing
Effective font pairing creates visual hierarchy through contrast. The classic approach pairs a serif heading font with a sans-serif body font (or vice versa). The key principle is meaningful contrast — if the fonts look too similar, the pairing feels arbitrary. If they clash stylistically, the design feels incoherent.
Proven pairing strategies:
- Same family — Use different weights within one typeface. Inter at 700 weight for headings, 400 for body. Simple and always harmonious
- Superfamily — Some typefaces come in both serif and sans-serif variants (Merriweather/Merriweather Sans, Noto Serif/Noto Sans). These are designed to work together
- Contrast pairing — A geometric sans-serif heading (Space Grotesk) with a humanist body font (IBM Plex Sans). The structural difference creates hierarchy while the humanist body font aids reading
/* Font pairing example: Space Grotesk + IBM Plex Sans */
:root {
--font-heading: 'Space Grotesk', system-ui, sans-serif;
--font-body: 'IBM Plex Sans', system-ui, sans-serif;
--font-code: 'JetBrains Mono', 'Fira Code', monospace;
}
h1, h2, h3, h4, h5, h6 {
font-family: var(--font-heading);
font-weight: 600;
line-height: 1.2;
}
body {
font-family: var(--font-body);
font-weight: 400;
line-height: 1.65;
}
code, pre {
font-family: var(--font-code);
font-weight: 400;
}
Typographic Scale
A typographic scale provides a consistent set of font sizes derived from a mathematical ratio. Instead of picking sizes arbitrarily (14px, 18px, 24px, 36px), a scale produces sizes that feel visually harmonious because they share a proportional relationship.
Common scales:
- 1.200 (Minor Third) — Compact, suitable for information-dense UIs
- 1.250 (Major Third) — The most versatile scale for general use
- 1.333 (Perfect Fourth) — More dramatic contrast between sizes
- 1.414 (Augmented Fourth) — Bold hierarchy, good for editorial designs
/* Major Third scale (1.250), base 1rem = 16px */
:root {
--text-xs: 0.64rem; /* 10.24px */
--text-sm: 0.8rem; /* 12.8px */
--text-base: 1rem; /* 16px */
--text-lg: 1.25rem; /* 20px */
--text-xl: 1.563rem; /* 25px */
--text-2xl: 1.953rem; /* 31.25px */
--text-3xl: 2.441rem; /* 39px */
--text-4xl: 3.052rem; /* 48.83px */
}
h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
h4 { font-size: var(--text-xl); }
p { font-size: var(--text-base); }
small { font-size: var(--text-sm); }
Fluid Typography
Fluid typography scales font sizes smoothly between a minimum and maximum value based on the viewport width. Instead of jumping between fixed sizes at breakpoints, text grows and shrinks proportionally with the screen. The CSS clamp() function makes this straightforward:
/* Fluid typography with clamp() */
/* clamp(minimum, preferred, maximum) */
h1 {
/* 32px at 320px viewport, scales to 64px at 1200px viewport */
font-size: clamp(2rem, 1.273rem + 3.636vw, 4rem);
line-height: 1.1;
}
h2 {
font-size: clamp(1.5rem, 1.136rem + 1.818vw, 2.5rem);
line-height: 1.2;
}
p {
/* 16px minimum, 18px maximum */
font-size: clamp(1rem, 0.909rem + 0.455vw, 1.125rem);
line-height: 1.65;
}
The preferred value in clamp() uses a combination of a fixed value and a viewport unit. This creates a linear interpolation between the minimum and maximum sizes. Online calculators like Utopia and fluid-type-scale.com generate these values from your minimum and maximum sizes and viewport widths.
Line Height, Measure, and Spacing
Line Height
Body text needs generous line spacing for comfortable reading. A line-height of 1.5 to 1.7 works for most body text. Headings need tighter line-height (1.1 to 1.3) because their larger size already creates vertical space. Using unitless values for line-height ensures the spacing scales proportionally with the font size.
Measure (Line Length)
Optimal line length for reading is 45 to 75 characters per line. Lines that are too short cause excessive eye movement from line to line. Lines that are too long cause readers to lose their place when returning to the left edge. The ch unit in CSS provides a direct way to control character count:
.article-content {
max-width: 65ch;
margin-inline: auto;
padding-inline: 1.5rem;
}
/* For wider layouts, use multi-column */
.wide-content {
max-width: 120ch;
columns: 2;
column-gap: 3rem;
}
Vertical Rhythm
Consistent vertical spacing creates a visual rhythm that makes pages feel organized. A common approach uses a base spacing unit derived from the body text line-height:
:root {
--space-unit: 1.5rem; /* Matches body line-height */
}
h2 {
margin-top: calc(var(--space-unit) * 3);
margin-bottom: var(--space-unit);
}
h3 {
margin-top: calc(var(--space-unit) * 2);
margin-bottom: calc(var(--space-unit) * 0.5);
}
p {
margin-bottom: var(--space-unit);
}
blockquote {
margin-block: calc(var(--space-unit) * 1.5);
padding-left: var(--space-unit);
border-left: 3px solid #c2724e;
}
Variable Fonts
Variable fonts contain an entire family of weights, widths, and other design axes in a single font file. Instead of loading separate files for Regular (400), Medium (500), Semibold (600), and Bold (700), one variable font file covers the full range. This can dramatically reduce the number of HTTP requests and total file size.
/* Using a variable font */
@font-face {
font-family: 'Inter';
src: url('/fonts/Inter-var.woff2') format('woff2-variations');
font-weight: 100 900; /* Full weight range */
font-display: swap;
}
/* You can use any weight value, not just 400, 500, 600, 700 */
.subtle-bold {
font-weight: 550; /* Between medium and semibold */
}
/* Animation with variable fonts */
.loading-text {
animation: weight-pulse 1.5s ease-in-out infinite;
}
@keyframes weight-pulse {
0%, 100% { font-weight: 300; }
50% { font-weight: 700; }
}
Beyond weight, variable fonts can expose custom axes: width (wdth), optical size (opsz), slant (slnt), and custom axes defined by the font designer. Recursive, a variable monospace font, includes axes for casual stroke treatment, giving a single font file both a standard and a cursive personality.
Font Loading and Performance
Web fonts are render-blocking by default. The browser delays text rendering until the font file downloads, causing a flash of invisible text (FOIT) or a flash of unstyled text (FOUT). Controlling this behavior is essential for web performance.
font-display Property
@font-face {
font-family: 'Space Grotesk';
src: url('/fonts/space-grotesk-var.woff2') format('woff2');
font-display: swap; /* Show fallback immediately, swap when loaded */
}
/* font-display values:
auto — Browser decides (usually block)
block — Short invisible period, then swap (causes FOIT)
swap — Show fallback immediately, swap when loaded (causes FOUT)
fallback — Very short invisible period, short swap window
optional — Very short invisible period, may skip web font entirely
*/
Preloading Critical Fonts
<!-- Preload fonts used above the fold -->
<link rel="preload" href="/fonts/space-grotesk-var.woff2"
as="font" type="font/woff2" crossorigin>
<link rel="preload" href="/fonts/ibm-plex-sans-var.woff2"
as="font" type="font/woff2" crossorigin>
Self-hosting vs CDN
Self-hosting fonts (serving them from your own domain) is now the recommended approach. Google Fonts previously benefited from a shared browser cache across websites, but modern browsers have partitioned their caches by origin, eliminating this advantage. Self-hosting gives you control over caching headers, avoids third-party DNS lookups, and eliminates the external dependency.
# Convert Google Fonts to self-hosted with google-webfonts-helper
# Or use the fontsource npm package
npm install @fontsource-variable/inter
npm install @fontsource-variable/space-grotesk
Subsetting
If you only use Latin characters, subset your fonts to remove Cyrillic, Greek, Arabic, and other character sets. This can reduce file sizes by 50-80%. The unicode-range descriptor in @font-face tells the browser to only download the font file if the page contains characters in that range.
System Font Stack
For applications where loading speed matters more than brand typography, the system font stack uses the operating system’s native font. This means zero font downloads, instant text rendering, and a look that feels native to the user’s platform:
body {
font-family:
-apple-system, /* macOS, iOS */
BlinkMacSystemFont, /* macOS Chrome */
'Segoe UI', /* Windows */
'Roboto', /* Android */
'Oxygen', /* KDE */
'Ubuntu', /* Ubuntu */
'Cantarell', /* GNOME */
'Fira Sans', /* Firefox OS */
'Droid Sans', /* Older Android */
'Helvetica Neue', /* Older macOS */
sans-serif;
}
/* Or the shorthand: */
body {
font-family: system-ui, -apple-system, sans-serif;
}
OpenType Features
Many professional fonts include OpenType features that enhance typographic quality. These features are off by default but can be activated with CSS:
.body-text {
/* Kerning: adjusts spacing between specific character pairs */
font-kerning: normal;
/* Ligatures: combines character pairs like fi, fl, ffi */
font-variant-ligatures: common-ligatures;
/* Tabular numbers: equal-width digits for tables and alignment */
font-variant-numeric: tabular-nums;
/* Old-style numbers: varying height digits for body text */
font-variant-numeric: oldstyle-nums;
}
.table-data {
/* Tabular, lining numbers: consistent width, consistent height */
font-variant-numeric: tabular-nums lining-nums;
}
/* Shorthand for all OpenType features */
.pro-typography {
font-feature-settings: 'kern' 1, 'liga' 1, 'calt' 1;
}
Dark Mode Typography
Text on dark backgrounds requires adjustments. White text on a black background appears heavier than black text on white due to a phenomenon called halation — light from bright text bleeds into the surrounding dark area. Compensate by reducing font weight slightly in dark mode and avoiding pure black backgrounds with pure white text:
@media (prefers-color-scheme: dark) {
body {
color: #e5e5e5; /* Not pure white */
background-color: #1c1917; /* Not pure black */
font-weight: 350; /* Slightly lighter than 400 (variable fonts) */
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
}
Frequently Asked Questions
How many fonts should I use on a single website?
Two fonts cover most needs — one for headings and one for body text. A third monospace font is appropriate for technical sites that display code. Loading more than three font families significantly impacts page load time and creates visual complexity that rarely adds value. If you need variety, explore different weights and styles within a single family rather than adding new families.
What is the ideal body font size for the web?
16px is the browser default and a reasonable minimum for body text. Most design-conscious sites use 18px (1.125rem) or 20px (1.25rem) for article content. Mobile screens, where users hold the device closer, can use slightly smaller sizes. The specific font also matters — Inter at 16px reads more comfortably than some other typefaces at the same size because of its tall x-height and open counters. Use fluid typography to scale appropriately across devices.
Are Google Fonts still a good choice for web projects?
The font quality available through Google Fonts is excellent. Inter, Space Grotesk, IBM Plex Sans, and many other high-quality typefaces are available there for free. However, serving fonts from the Google Fonts CDN is no longer recommended due to browser cache partitioning and GDPR privacy concerns in Europe. Download the font files and self-host them for best performance and privacy compliance.
How do variable fonts affect page load performance?
A variable font replaces multiple static font files with a single file. If you use three or more weights from a family, the variable font file is typically smaller than the combined static files. For example, loading Inter in Regular, Medium, Semibold, and Bold as static files totals around 140KB. The Inter variable font is approximately 95KB. The savings increase with each additional weight you use. For sites that only use one or two weights, static fonts may be slightly smaller.
Typography is where design meets responsive engineering. Well-chosen fonts with proper loading, sizing, and spacing create reading experiences that hold attention and communicate professionalism. Combined with a solid framework, good accessibility practices, and thoughtful use of modern development tools, strong typography elevates the entire user experience. As the web continues to mature, the typographic quality users expect continues to rise with it.