022: Typography
Learning outcomes
By the end, you can build a readable font stack; size text with relative units; control line height, measure, weight, alignment, and spacing; create a clear type hierarchy; load web fonts responsibly; and test zoom and reflow.
Prerequisites and retrieval
Use the palette from 021. Retrieve which text properties inherit and why foreground/background contrast is a pair. Read the portfolio without CSS: its heading structure must still make sense before visual hierarchy is added.
Terminology
- Typeface/font family: The named visual design of text; font-family selects from available families. — Source: MDN: font-family
- Font stack: An ordered list of fallback families in font-family, ending with a generic family. — Source: MDN: font-family
- Generic family: Keyword categories such as serif, sans-serif, monospace, system-ui. — Source: MDN: font-family
- Root em (
rem): A length unit relative to the root element’s font size. — Source: CSS Values 4: rem - Em (
em): Relative to the parent font size for font-size, else the element’s own font size. — Source: CSS Values 4: font-relative lengths - Line height/leading: The vertical space allocated to each line box via line-height. — Source: MDN: Styling text
- Measure: The length of a line of text, commonly constrained with ch units. — Source: CSS Values 4: ch
- Weight: Text stroke thickness on the 100–900 scale (400 normal, 700 bold). — Source: MDN: Fundamental text styling
- Web font: A font file downloaded by the page via @font-face. — Source: MDN: Web fonts
- Typography (formal): "Typography is the art of arranging type — font, size, line height, measure, and hierarchy — for legible reading." — Source: MDN: Styling text
- Character unit (
ch): "ch represents the width of the 0 glyph in the element's font." — Source: CSS Values and Units Level 4: ch - System font stack: "A font-family list starting with system-ui that resolves to the platform UI font." — Source: MDN: System fonts
Mental model: typography is a reading system
Typography is not choosing a decorative font. It coordinates family, size, line height, weight, width, spacing, and contrast so readers can scan hierarchy and sustain reading. Browser defaults honor user preferences; relative units preserve that adaptability.
1rem normally resolves to the root font size. Do not set the root to 62.5% just to imitate decimal pixels; that weakens direct reasoning and can conflict with expectations. Keep body text near the user default and create hierarchy with modest ratios. Unitless line-height is inherited as a multiplier, allowing each descendant to calculate spacing from its own font size.
Long lines tire readers. max-inline-size: 65ch limits a prose block to roughly 65 zero-character widths while permitting it to shrink on narrow screens. Avoid fixed heights around text; zoom, translations, and fallback fonts change wrapping.
Beginner example: portfolio type hierarchy
body {
margin: 0;
color: rgb(30 41 59);
background: rgb(248 250 252);
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
font-size: 1rem;
line-height: 1.6;
}
h1,
h2,
h3 {
color: rgb(15 23 42);
line-height: 1.2;
}
h1 {
margin-block: 0 0.5rem;
font-size: 2.5rem;
letter-spacing: -0.025em;
}
h2 {
margin-block: 2rem 0.75rem;
font-size: 1.75rem;
}
h3 {
margin-block: 0 0.5rem;
font-size: 1.25rem;
}
p,
li {
max-inline-size: 65ch;
}
.intro {
font-size: 1.125rem;
color: rgb(71 85 105);
}
.project-meta {
font-size: 0.875rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
}
Add HTML:
<header>
<h1>Asha Rao</h1>
<p class="intro">Frontend developer focused on inclusive interfaces.</p>
</header>
<main>
<section aria-labelledby="work-title">
<h2 id="work-title">Selected work</h2>
<article class="project">
<p class="project-meta">HTML · Accessibility · 2026</p>
<h3>Library finder</h3>
<p>A clearer route from location search to opening hours.</p>
</article>
</section>
</main>
The HTML level, not font size, determines heading meaning. The CSS makes those levels visually distinct. Body line height is generous; headings use a tighter value because large display text needs less relative leading. Slight negative heading letter spacing can suit large system text, while uppercase metadata needs extra spacing. Never apply wide tracking to long paragraphs.
Zoom to 200%. Text should enlarge, wrap, and remain present. If a box clips it, the box's fixed dimensions are the problem.
Intermediate example: readable article content
<article class="case-study">
<header>
<p class="case-study__kicker">Case study</p>
<h1>Making library search understandable</h1>
<p class="case-study__summary">A semantic redesign tested with keyboard navigation and browser zoom.</p>
</header>
<h2>The problem</h2>
<p>Visitors needed opening hours, but labels and link text were ambiguous.</p>
<blockquote>
<p>Clear words reduced hesitation more than visual decoration.</p>
</blockquote>
</article>
.case-study {
max-inline-size: 70ch;
margin-inline: auto;
padding-inline: 1rem;
}
.case-study__kicker {
margin-block-end: 0.25rem;
color: rgb(29 78 216);
font-weight: 750;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.case-study__summary {
font-size: 1.25rem;
line-height: 1.5;
}
.case-study blockquote {
margin-inline: 0;
padding-inline-start: 1rem;
border-inline-start: 0.25rem solid rgb(37 99 235);
color: rgb(51 65 85);
font-size: 1.125rem;
}
.case-study a {
color: rgb(29 78 216);
text-decoration-thickness: 0.1em;
text-underline-offset: 0.18em;
}
margin-inline: auto centers a block with constrained inline size; the content can still fill a narrow viewport. Logical properties support different writing directions. Avoid justified body text on the web: uneven spacing can create distracting rivers, especially in narrow columns.
Optional advanced example: responsible web fonts
System fonts are fast and familiar. If the design genuinely needs a local web font:
@font-face {
font-family: "Portfolio Sans";
src: url("fonts/portfolio-sans.woff2") format("woff2");
font-style: normal;
font-weight: 400 800;
font-display: swap;
}
body {
font-family: "Portfolio Sans", system-ui, sans-serif;
}
WOFF2 is an efficient web format. A variable font file can cover a weight range, shown as 400 800, if the actual file supports it. font-display: swap keeps text visible using fallback text while the font loads. Licensing must permit web embedding. Limit families, styles, scripts, and weights to what the page uses, and test layout shift caused by different fallback metrics.
Mistakes, debugging, and DevTools
- Missing generic fallback: if the preferred font fails, results become unpredictable.
- Quoting incorrectly: multi-word family names require quotes; generic names do not.
- Setting body text in pixels and assuming users cannot override it: use relative sizing and test browser settings.
- Using
line-height: 20px: descendants with larger type may collide; prefer unitless values. - Centering long paragraphs, full uppercase prose, or excessive letter spacing: all reduce sustained readability.
- Restricting text with fixed width/height: content clips under zoom or translation.
- Faking hierarchy with font size while HTML headings are out of order: CSS cannot repair document semantics.
- Loading many font files: slow text rendering and page weight grow quickly.
Inspect text in DevTools. Computed styles show final family, size, line height, and inherited sources. Many browsers have a Fonts panel that reports which font actually rendered; a listed CSS family may not be available. In Network, filter by “Font” and inspect status, transfer size, timing, and CORS errors. Disable the custom face to test the fallback.
Accessibility and performance
WCAG text-resize guidance requires content to remain usable when text is resized to 200% without assistive technology. Reflow guidance expects content at narrow equivalent widths without two-dimensional scrolling, except genuinely two-dimensional content. Do not prevent user zoom in the viewport meta tag.
For paragraphs, a line height around 1.5 is a useful baseline, not an absolute law. WCAG 1.4.12 also requires no loss when users override spacing to at least 1.5 line height, 2 times font size paragraph spacing, 0.12 times letter spacing, and 0.16 times word spacing. Flexible boxes and no fixed text heights help.
Fonts can delay rendering and shift layout. Prefer system fonts when branding does not demand a download. Otherwise use WOFF2, only needed subsets/weights, appropriate caching, and visible fallback behavior. Never render essential text as an image.
Deep dive: every major text control
A useful typography system understands what each property is responsible for:
.article {
font-family: ui-serif, Georgia, serif;
font-size: 1rem;
font-weight: 400;
font-style: normal;
line-height: 1.7;
letter-spacing: 0;
word-spacing: normal;
text-align: start;
text-decoration: none;
text-transform: none;
}
font-family chooses the typeface stack. font-size sets the em size. font-weight selects weight. line-height controls line-box height. Spacing and decoration properties should solve actual reading or UI needs rather than imitate arbitrary design screenshots.
Font shorthand
The font shorthand can set several font properties at once:
.card-title {
font: 700 1.25rem/1.25 system-ui, sans-serif;
}
This is compact, but omitted subproperties may reset. Use longhands while learning or when a partial change is clearer.
Deep dive: web-font loading
A local font file can be declared with @font-face:
@font-face {
font-family: "InterCourse";
src: url("/fonts/inter-var.woff2") format("woff2");
font-weight: 100 900;
font-style: normal;
font-display: swap;
}
body {
font-family: "InterCourse", system-ui, sans-serif;
}
Important considerations:
- Prefer WOFF2 for modern web delivery.
- Subset font files when the project controls the font pipeline.
- Avoid loading many families and weights without a clear need.
font-display: swapallows fallback text to render instead of staying invisible while the font downloads.- Test layout shift because fallback and web fonts can have different metrics.
A hosted provider such as Google Fonts can simplify setup, but local/self-hosted fonts offer more control over privacy, caching, and request policy.
Worked example: responsive type without breakpoint piles
.hero-title {
font-size: clamp(2rem, 1.35rem + 3vw, 4.5rem);
line-height: 1.02;
letter-spacing: -0.025em;
max-inline-size: 14ch;
}
.hero-copy {
font-size: clamp(1rem, 0.95rem + 0.35vw, 1.25rem);
line-height: 1.6;
max-inline-size: 60ch;
}
clamp(minimum, preferred, maximum) gives the browser a fluid range while protecting readability at both ends. The preferred expression should not be so viewport-heavy that user zoom becomes ineffective; always test zoom.
Worked example: link decoration that survives color limitations
.article a {
color: #1d4ed8;
text-decoration-line: underline;
text-decoration-thickness: 0.12em;
text-underline-offset: 0.18em;
}
.article a:hover {
text-decoration-thickness: 0.2em;
}
.article a:focus-visible {
outline: 3px solid #f59e0b;
outline-offset: 3px;
}
Links remain distinguishable even if color perception is reduced.
Deep dive: measure, rhythm, and wrapping
Readable long-form text usually needs a bounded line measure:
.prose {
max-inline-size: 68ch;
margin-inline: auto;
}
ch approximates the width of the 0 glyph and is useful for text measure, not exact pixel geometry.
Headings can benefit from balanced wrapping when supported:
h1,
h2 {
text-wrap: balance;
}
Paragraphs may use:
.prose p {
text-wrap: pretty;
}
These are enhancements, not replacements for sensible width constraints.
Deep dive: text transformations and shadows
.eyebrow {
text-transform: uppercase;
letter-spacing: 0.08em;
}
Do not store essential text in the wrong case solely to compensate with CSS. Screen readers and copy/paste behavior should receive meaningful source text.
Text shadows can improve a decorative headline but should not be required for readability:
.hero-title {
text-shadow: 0 2px 12px rgb(0 0 0 / 0.35);
}
If text needs a heavy shadow just to become legible over a photograph, fix the underlying contrast with a scrim/overlay or different image crop.
Typography stress test
Test each page with:
- browser zoom at 200%;
- text-only zoom where available;
- a very long heading;
- a long unbroken URL or identifier;
- font loading disabled/throttled;
- a system font fallback;
- narrow viewport;
- high-contrast/forced-color mode where available.
A typography system is successful when the content remains readable under stress, not only when the preferred font finishes loading instantly.
Font variants and numeric UI
.invoice-total,
.metric {
font-variant-numeric: tabular-nums;
}
Tabular numerals give digits equal advance widths, which can improve alignment in changing financial/dashboard values.
Other font-variant-* features exist for ligatures, caps, east-Asian text, and more. Use them only when the selected font supports the feature and the content benefits.
Hosted fonts versus self-hosting
A provider such as Google Fonts may be included with stylesheet links, while self-hosting uses your own @font-face files. Compare:
- privacy/policy requirements;
- cache behavior;
- number of external connections;
- control over font files/subsets;
- operational simplicity.
Do not include ten font weights when the design uses only 400 and 700.
Tiered exercises
Checkpoint: read under stress
Replace one short paragraph with several sentences, enlarge the browser's default font, and narrow the window. Scan the heading, summary, metadata, body, and link in order. Hierarchy should remain apparent without relying on color alone, and no line should disappear behind a fixed box. This content stress test is more informative than judging a perfectly short mockup.
Then disable the preferred font in DevTools. Check whether fallback metrics cause clipped labels, overlapping controls, or an unacceptable layout shift. A font stack is a sequence of real rendering outcomes, not decorative documentation. If the fallback is substantially wider, flexible containers and wrapping should absorb the difference. Keep actual bold and italic faces available when those distinctions convey meaning.
Foundation: Set a system font stack and unitless body line height. Create distinct h1, h2, and h3 sizes in rem without changing HTML order.
Core: Limit prose measure, style intro and metadata text, and verify the portfolio at 200% zoom and a narrow window.
Stretch: Add one licensed WOFF2 font with a system fallback and font-display: swap. Compare Network transfer and fallback behavior; remove it if it does not improve the design enough to justify its cost.
body {
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
font-size: 1rem;
line-height: 1.6;
}
h1, h2, h3 { line-height: 1.2; color: rgb(15 23 42); }
h1 { font-size: 2.5rem; letter-spacing: -0.025em; }
h2 { font-size: 1.75rem; }
h3 { font-size: 1.25rem; }
p, li { max-inline-size: 65ch; }
.intro { font-size: 1.125rem; color: rgb(71 85 105); }
.project-meta {
font-size: 0.875rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
}
.case-study { max-inline-size: 70ch; margin-inline: auto; padding-inline: 1rem; }
a { text-underline-offset: 0.18em; }
a:focus-visible { outline: 3px solid rgb(249 115 22); outline-offset: 3px; }
Recap and exit questions
Typography is an adaptive reading system. Relative sizes, unitless line height, restrained measure, robust fallbacks, and semantic headings create more value than decorative font choices.
- How do
remandemdiffer? - Why is unitless line height useful?
- What problem does
max-inline-size: 65chaddress? - Why can a loaded font still render poorly while it arrives?
- What tests should pass at 200% zoom?
