Implement production-quality typography — choose type scales with sufficient contrast, set optimal line length and line height, load web fonts efficiently, and use CSS clamp() for fluid typography that scales across all viewports.
Typography accounts for 95% of web design according to design historian Oliver Reichenstein. Text is the primary medium for communicating on the web — how it is set determines whether users can read it efficiently and whether the product feels professional.
For developers, typography decisions manifest in CSS: font-size, line-height, letter-spacing, max-width, and font-face loading. Understanding the principles behind each choice lets you evaluate designs accurately and implement them correctly without guessing.
A type scale is a set of font sizes chosen for harmonic relationships. Instead of choosing arbitrary sizes (14, 16, 18, 22, 28...), a scale uses a consistent ratio to generate all sizes.
Common type scale ratios:
1Minor Third (1.2): 12, 14.4, 17.3, 20.7, 24.9, 29.9...
2Major Third (1.25): 12, 15, 18.75, 23.4, 29.3...
3Fourth (1.333): 12, 16, 21.3, 28.4, 37.9...
4Fifth (1.5): 12, 18, 27, 40.5, 60.75...For UI design, the Major Third (1.25) and Perfect Fourth (1.333) ratios produce clear hierarchy without excessive jumps.
DevBootstrap type scale (Perfect Fourth base 16px):
1:root {
2 /* Scale: 1.333 ratio, base 16px */
3 --text-xs: 0.75rem; /* 12px — legal, micro labels */
4 --text-sm: 0.875rem; /* 14px — captions, metadata */
5 --text-base: 1rem; /* 16px — body text base */
6 --text-lg: 1.125rem; /* 18px — slightly larger body */
7 --text-xl: 1.25rem; /* 20px — card titles, callouts */
8 --text-2xl: 1.5rem; /* 24px — subsection headings */
9 --text-3xl: 1.875rem; /* 30px — section headings */
10 --text-4xl: 2.25rem; /* 36px — page titles */
11 --text-5xl: 3rem; /* 48px — hero headings */
12 --text-6xl: 3.75rem; /* 60px — very large display */
13 --text-7xl: 4.5rem; /* 72px — hero display at large screens */
14}Mapping scale to HTML elements:
1h1 { font-size: var(--text-4xl); } /* 36px default, fluid with clamp() */
2h2 { font-size: var(--text-3xl); } /* 30px */
3h3 { font-size: var(--text-2xl); } /* 24px */
4h4 { font-size: var(--text-xl); } /* 20px */
5p { font-size: var(--text-base); } /* 16px */
6
7/* Label/metadata */
8.caption {
9 font-size: var(--text-sm); /* 14px */
10 color: var(--color-text-muted);
11}
12
13/* Micro labels */
14.badge-text {
15 font-size: var(--text-xs); /* 12px */
16 font-weight: 600;
17 text-transform: uppercase;
18 letter-spacing: 0.05em; /* uppercase at small sizes benefits from tracking */
19}Readability is determined primarily by three properties: font-size, line-height, and line-length (max-width). These three work together.
Optimal line height by text size:
1Small text (< 16px): line-height: 1.5–1.6
2Body text (16-18px): line-height: 1.5–1.7
3Large text (> 24px): line-height: 1.2–1.4
4Headings (> 36px): line-height: 1.1–1.25Why headings have tighter line height: at 36px, 1.6 line height creates too much gap between lines (57.6px per line). At 1.2, the gap is 43.2px — enough breathing room without making the heading feel disconnected.
CSS implementation:
1/* Body text: generous line height for extended reading */
2body {
3 font-size: var(--text-base); /* 16px */
4 line-height: 1.6; /* 25.6px line height */
5 color: var(--color-text);
6}
7
8/* Headings: progressively tighter line height as size increases */
9h1 { line-height: 1.2; } /* tight — large type needs less */
10h2 { line-height: 1.25; }
11h3 { line-height: 1.3; }
12h4 { line-height: 1.35; }
13p { line-height: 1.6; } /* loose — small type needs more */
14
15.caption { line-height: 1.4; } /* captions: between heading and body */Optimal line length (45-75 characters):
1/* Optimal reading width */
2.prose {
3 max-width: 65ch; /* ch unit = width of '0' character = ~character count */
4 /* 65ch ≈ 65 characters per line — top of optimal range */
5}
6
7.narrow-prose {
8 max-width: 45ch; /* for shorter, punchy content */
9}
10
11/* Alternative using rem */
12.article-body {
13 max-width: 40rem; /* ~640px — approximately 65-75 chars at 16px */
14}
15
16/* Why it matters: too wide = eyes get lost finding next line */
17/* Too narrow = too many line breaks, reading interrupted */
18
19/* Width extremes to avoid: */
20/* max-width: 100% → prose too wide at desktop */
21/* max-width: 30ch → too narrow, choppy reading */LetterSpacing:
1/* Body text: no letter spacing (default tracking is optimized) */
2body { letter-spacing: 0; }
3
4/* All-caps small text: needs tracking to be readable */
5.badge, .label {
6 letter-spacing: 0.05em; /* 5% of font-size */
7}
8
9/* Large headings: slight negative tracking is more elegant */
10.hero-heading { letter-spacing: -0.025em; }
11
12/* Never: positive tracking on body text */
13/* It reduces readability and looks like design amateur hour */CSS clamp() creates typography that scales smoothly between mobile and desktop without breakpoints.
clamp() syntax:
1/* clamp(minimum, preferred, maximum) */
2font-size: clamp(1.5rem, 3vw + 0.5rem, 3rem);
3/* Minimum: 1.5rem (24px) — never smaller */
4/* Preferred: 3vw + 0.5rem — scales with viewport */
5/* Maximum: 3rem (48px) — never larger */Fluid type scale for DevBootstrap:
1:root {
2 /* Each size scales from mobile minimum to desktop maximum */
3 --text-fluid-h1: clamp(2rem, 4vw + 0.5rem, 3.75rem);
4 /* At 375px: 2rem (32px) minimum */
5 /* At 1440px: 3.75rem (60px) maximum */
6 /* Smooth scaling between these breakpoints */
7
8 --text-fluid-h2: clamp(1.5rem, 3vw + 0.5rem, 2.5rem);
9 --text-fluid-h3: clamp(1.25rem, 2vw + 0.5rem, 1.875rem);
10 --text-fluid-h4: clamp(1.125rem, 1.5vw + 0.5rem, 1.5rem);
11 --text-fluid-body: clamp(1rem, 0.5vw + 0.875rem, 1.125rem);
12 /* Body barely scales: 16px mobile → 18px desktop */
13}
14
15h1 { font-size: var(--text-fluid-h1); }
16h2 { font-size: var(--text-fluid-h2); }
17p { font-size: var(--text-fluid-body); }Calculating clamp() values from two breakpoints:
1Given:
2 Mobile (375px = 23.4375rem): font-size = 2rem
3 Desktop (1440px = 90rem): font-size = 3.75rem
4
5Formula:
6 slope = (max - min) / (max-vw - min-vw)
7 = (3.75 - 2) / (90 - 23.4375)
8 = 1.75 / 66.5625
9 = 0.0263
10
11 offset = min - slope × min-vw
12 = 2 - 0.0263 × 23.4375
13 = 2 - 0.617
14 = 1.383rem
15
16 Result: clamp(2rem, 2.63vw + 1.383rem, 3.75rem)
17 Simplified: clamp(2rem, 2.6vw + 1.4rem, 3.75rem)
18
19 Tool: utopia.fyi generates these formulas automaticallyWeb font loading is one of the primary causes of Cumulative Layout Shift (CLS) and render-blocking. Correct font loading eliminates these performance problems.
Font loading checklist:
1<!-- 1. Preconnect to font origin (reduces DNS lookup time) -->
2<link rel="preconnect" href="https://fonts.googleapis.com">
3<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
4
5<!-- 2. Preload the most critical font file (body font only) -->
6<link
7 rel="preload"
8 href="/fonts/inter-variable.woff2"
9 as="font"
10 type="font/woff2"
11 crossorigin
12>1/* 3. font-display: swap — show fallback immediately, swap when loaded */
2@font-face {
3 font-family: 'Inter';
4 font-style: normal;
5 font-weight: 100 900; /* variable font — all weights in one file */
6 font-display: swap; /* CRITICAL: prevents FOIT (flash of invisible text) */
7 src:
8 url('/fonts/inter-variable.woff2') format('woff2');
9}
10
11/* 4. Fallback font stack that closely matches Inter dimensions */
12body {
13 font-family: 'Inter', system-ui, -apple-system, BlinkMacSystemFont,
14 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
15}Reducing CLS from font loading:
1/* 5. size-adjust: adjusts fallback font to match web font metrics */
2@font-face {
3 font-family: 'InterFallback';
4 src: local('Arial'); /* use system font as fallback */
5 ascent-override: 90%;
6 descent-override: 22%;
7 line-gap-override: 0%;
8 size-adjust: 107%; /* adjust dimensions to match Inter */
9}
10
11body {
12 font-family: 'Inter', 'InterFallback', system-ui, sans-serif;
13}
14/* When Inter loads, layout barely shifts — dimensions are pre-matched */Variable fonts (most efficient):
1/* One file for all weights and styles */
2@font-face {
3 font-family: 'Inter';
4 src: url('/fonts/inter.var.woff2') format('woff2');
5 font-weight: 100 900; /* entire weight range */
6 font-style: normal;
7 font-display: swap;
8}
9
10/* Use any weight without additional file requests */
11.heading { font-weight: 700; } /* uses the same file as 400 weight */
12.body { font-weight: 400; }
13.label { font-weight: 500; }Implementing the complete typography foundation — scale, fluid sizes, line heights, line lengths, and efficient font loading.
1/* HTML: Efficient font loading */
2/*
3<head>
4 <link rel="preconnect" href="https://fonts.googleapis.com">
5 <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
6 <!-- OR self-hosted: -->
7 <link rel="preload" href="/fonts/inter-variable.woff2" as="font" type="font/woff2" crossorigin>
8</head>
9*/
10
11/* CSS: Complete typography foundation */
12
13/* 1. Font face declaration */
14@font-face {
15 font-family: 'Inter';
16 src: url('/fonts/inter-variable.woff2') format('woff2');
17 font-weight: 100 900;
18 font-style: normal;
19 font-display: swap;
20}
21
22/* Fallback font matching Inter's metrics (reduces CLS) */
23@font-face {
24 font-family: 'InterFallback';
25 src: local('Arial');
26 ascent-override: 90%;
27 descent-override: 22%;
28 size-adjust: 107%;
29}
30
31/* 2. Base typography reset */
32*, *::before, *::after {
33 box-sizing: border-box;
34}
35
36html {
37 font-size: 100%; /* 1rem = 16px (user-controlled base) */
38 text-size-adjust: 100%; /* prevent iOS text inflation */
39 -webkit-text-size-adjust: 100%;
40}
41
42body {
43 font-family: 'Inter', 'InterFallback', system-ui, -apple-system, sans-serif;
44 font-size: 1rem; /* 16px */
45 line-height: 1.6; /* 25.6px — optimal for reading */
46 color: var(--color-text); /* from token system */
47 font-weight: 400;
48 font-feature-settings: 'liga' 1, 'kern' 1; /* ligatures + kerning */
49 -webkit-font-smoothing: antialiased; /* macOS smoothing */
50 -moz-osx-font-smoothing: grayscale;
51}
52
53/* 3. Type scale */
54:root {
55 --text-xs: 0.75rem; /* 12px */
56 --text-sm: 0.875rem; /* 14px */
57 --text-md: 1rem; /* 16px */
58 --text-lg: 1.125rem; /* 18px */
59 --text-xl: 1.25rem; /* 20px */
60 --text-2xl: 1.5rem; /* 24px */
61 --text-3xl: 1.875rem; /* 30px */
62 --text-4xl: 2.25rem; /* 36px */
63
64 /* Fluid headings */
65 --text-fluid-h1: clamp(2rem, 3.5vw + 0.75rem, 3.75rem);
66 --text-fluid-h2: clamp(1.5rem, 2.5vw + 0.5rem, 2.5rem);
67 --text-fluid-h3: clamp(1.25rem, 1.5vw + 0.5rem, 1.875rem);
68}
69
70/* 4. Heading elements */
71h1, h2, h3, h4, h5, h6 {
72 font-weight: 700;
73 color: var(--color-text);
74 text-wrap: balance; /* modern: prevents orphaned words */
75 margin-top: 0;
76}
77
78h1 {
79 font-size: var(--text-fluid-h1);
80 line-height: 1.2;
81 letter-spacing: -0.025em;
82}
83
84h2 {
85 font-size: var(--text-fluid-h2);
86 line-height: 1.25;
87 letter-spacing: -0.02em;
88}
89
90h3 {
91 font-size: var(--text-fluid-h3);
92 line-height: 1.3;
93 letter-spacing: -0.01em;
94}
95
96h4 {
97 font-size: var(--text-xl);
98 line-height: 1.35;
99 font-weight: 600;
100}
101
102/* 5. Prose containers */
103.prose {
104 max-width: 65ch; /* optimal reading width */
105 font-size: var(--text-md);
106 line-height: 1.7; /* slightly generous for prose */
107 color: var(--color-text);
108}
109
110.prose p { margin: 0 0 1em; }
111.prose p:last-child { margin-bottom: 0; }
112
113/* 6. Code typography */
114code, kbd, samp, pre {
115 font-family: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', monospace;
116 font-size: 0.9em; /* slightly smaller than surrounding text */
117 font-feature-settings: 'liga' 1; /* enable ligatures (!=, =>) */
118}
119
120pre {
121 line-height: 1.5; /* slightly tighter than prose */
122 overflow-x: auto;
123 tab-size: 2;
124}
125
126/* 7. Micro-typography utilities */
127.label-uppercase {
128 font-size: var(--text-xs);
129 font-weight: 600;
130 text-transform: uppercase;
131 letter-spacing: 0.08em; /* uppercase needs extra tracking */
132 color: var(--color-text-muted);
133}
134
135.text-numeric {
136 font-variant-numeric: tabular-nums; /* fixed-width digits for tables/prices */
137}The typography system starts with efficient font loading (font-display: swap eliminates FOIT) and a size-adjusted fallback (reduces CLS). The type scale uses rem for all sizes (respects user browser font size preferences). Fluid heading sizes use clamp() to scale smoothly. The prose container uses 65ch max-width — the ch unit automatically adjusts to the actual character width of the loaded font, making it a more reliable measure than px or rem for controlling line length. The text-wrap: balance on headings is a modern CSS feature that prevents orphaned single words on the last line of headings.
The next lesson covers Layout and Composition — how to arrange elements on a page using CSS Grid and Flexbox to create balanced, scannable page structures.
Your progress is saved in this browser, on this device only.