Responsive Typography
Overview
You will build a blog article page with a fully working modular type system — no media queries. By the end of the session, type will scale fluidly across any screen size, and the article will adapt its heading sizes based on its own container width, not the browser window.
Learning Objectives
By the end of this lesson, you will be able to:
Explain why viewport-based media queries are often the wrong tool for typography
Calculate a modular type scale using a ratio
Use clamp() to create fluid font sizes that scale between a minimum and maximum
Define and apply a type system using CSS custom properties
Write container queries that respond to an element's own width
Deliverable
👉 Please uploaded completed, fully functional HTML and CSS to Canvas.
Starting Files
Please download and extract the following files to proceed:
Snippets
Quick Reference — Scale at a Glance
Step | Approx. Size | Use It For |
--step-5 | ~4rem | Site title / hero heading |
--step-4 | ~3rem | Article title (h2) |
--step-3 | ~2.4rem | Section heading (h3) |
--step-2 | ~1.8rem | Sub-heading (h4), blockquote |
--step-1 | ~1.1rem | Byline, tags, captions, footer |
--step-0 | ~1rem | Body text, list items |
1 - Import Your Fonts
Paste this inside the <head> of your HTML, above your <style> tag. This loads two fonts from Google Fonts — one for body text (Lora, a serif) and one for UI elements (DM Sans, a sans-serif).
<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=Lora:ital,wght@0,400;0,600;1,400&family=DM+Sans:wght@400;500&family=DM+Mono&display=swap" rel="stylesheet">2 - Build The Type Scale
This goes inside :root. These are your custom properties — the entire type system lives here. Every font size in the rest of the file will reference one of these steps.
:root {
font-size: 1rem; /* Fixed anchor. Do not change. */
/* Modular scale — Perfect Fourth (1.333) */
--step-0: 1rem;
--step-1: clamp(0.889rem, 0.3vw + 0.814rem, 1.111rem);
--step-2: clamp(1.185rem, 0.65vw + 1rem, 1.481rem);
--step-3: clamp(1.481rem, 1.1vw + 1.2rem, 2.369rem);
--step-4: clamp(1.777rem, 2vw + 1.25rem, 3.157rem);
--step-5: clamp(2.369rem, 3.5vw + 1.5rem, 4.209rem);
/* Font families */
--font-body: 'Lora', Georgia, serif;
--font-ui: 'DM Sans', system-ui, sans-serif;
--font-mono: 'DM Mono', 'Courier New', monospace;
}Each clamp() has three parts: a minimum, a fluid middle value, and a maximum. The middle value combines vw (grows with the viewport) and rem (prevents it from ever collapsing to zero).
3 - Header Type
This targets the h1 and subtitle p inside the <header>.
header h1 {
font-family: var(--font-body);
font-size: var(--step-5);
font-weight: 600;
line-height: 1.1;
letter-spacing: -0.02em;
}
header p {
font-family: var(--font-ui);
font-size: var(--step-1);
color: var(--color-rule);
margin-top: var(--space-xs);
letter-spacing: 0.08em;
text-transform: uppercase;
}Large headings need a tighter line-height (around 1.1) because the optical gap between lines grows with type size. Negative letter-spacing counteracts the optical looseness that happens at large sizes.
4 - Article Headings
h2 {
font-family: var(--font-body);
font-size: var(--step-4);
font-weight: 600;
line-height: 1.15;
letter-spacing: -0.02em;
margin-top: var(--space-lg);
margin-bottom: var(--space-sm);
}
h3 {
font-family: var(--font-body);
font-size: var(--step-3);
font-weight: 600;
line-height: 1.2;
letter-spacing: -0.015em;
margin-top: var(--space-md);
margin-bottom: var(--space-xs);
}
h4 {
font-family: var(--font-ui);
font-size: var(--step-2);
font-weight: 500;
line-height: 1.3;
margin-top: var(--space-md);
margin-bottom: var(--space-xs);
}Notice h4 switches to --font-ui. Mixing a serif and sans-serif across heading levels creates a clear hierarchy without relying on size alone.
5 - Body Text & Drop Cap
p {
font-family: var(--font-body);
font-size: var(--step-0);
max-width: var(--measure-narrow);
margin-bottom: var(--space-sm);
}
/* Bonus: drop cap on the first paragraph */
article > p:first-of-type::first-letter {
font-size: 3.5em;
font-weight: 600;
line-height: 0.8;
float: left;
margin-right: 0.1em;
margin-top: 0.08em;
color: var(--color-accent);
}max-width: var(--measure-narrow) keeps lines at roughly 52 characters — the sweet spot for comfortable reading. The drop cap uses em units so it scales relative to the paragraph's own font size.
6 - Byline & Tags
.byline {
font-family: var(--font-ui);
font-size: var(--step-1);
color: var(--color-ink-muted);
margin-bottom: var(--space-xs);
max-width: none;
}
.tags li a {
font-family: var(--font-ui);
font-size: var(--step-1);
letter-spacing: 0.04em;
text-decoration: none;
background-color: var(--color-tag-bg);
color: var(--color-tag-ink);
padding: 0.25em 0.75em;
border-radius: 2px;
transition: background-color 0.15s ease, color 0.15s ease;
}
Both byline and tags sit one step below body text (--step-1). Supporting text should never compete with the content hierarchy.
7 - Blockquote
blockquote {
font-family: var(--font-body);
font-size: var(--step-2);
font-style: italic;
line-height: 1.5;
color: var(--color-ink-muted);
border-left: 3px solid var(--color-accent);
padding: var(--space-sm) var(--space-md);
margin: var(--space-md) 0;
background-color: var(--color-accent-bg);
max-width: var(--measure-narrow);
}The blockquote steps up to --step-2 so it has presence as a pulled quote. font-style: italic reinforces that it's a voice distinct from the body.
8 - List Items
ul:not(.tags) li {
font-size: var(--step-0);
margin-bottom: 0.4em;
max-width: var(--measure-narrow);
}List items match body text size (--step-0). The :not(.tags) selector ensures this rule doesn't accidentally apply to the tag links at the top of the article.
9 - Inline Code
code {
font-family: var(--font-mono);
font-size: 0.875em;
background-color: var(--color-tag-bg);
padding: 0.1em 0.4em;
border-radius: 3px;
color: var(--color-accent);
}font-size: 0.875em is relative to the parent — not a --step-* value. Monospace fonts read visually larger than proportional fonts at the same size, so we nudge them down slightly. Using em here means it works correctly whether the code is inside a heading or a paragraph.
10 - Figcaption
figcaption {
font-family: var(--font-ui);
font-size: var(--step-1);
font-style: italic;
color: var(--color-ink-muted);
margin-top: var(--space-xs);
padding-left: 0.5em;
border-left: 2px solid var(--color-rule);
}Captions use --step-1 — the same as bylines and tags. Grouping supporting text on the same scale step creates visual consistency across the page.
11 - Container Queries
These rules fire based on the article element's own width — not the browser window. The three blocks handle narrow, mid, and wide containers.
/* Narrow container — drop headings one scale step */
@container article (max-width: 480px) {
h2 { font-size: var(--step-3); }
h3 { font-size: var(--step-2); }
h4 { font-size: var(--step-1); }
blockquote {
font-size: var(--step-1);
padding: var(--space-sm);
}
}
/* Mid-width — soften h2 and blockquote slightly */
@container article (min-width: 481px) and (max-width: 680px) {
h2 { font-size: var(--step-3); letter-spacing: -0.015em; }
blockquote { font-size: var(--step-1); }
}
/* Wide — let the drop cap breathe */
@container article (min-width: 681px) {
article > p:first-of-type::first-letter {
font-size: 4.5em;
}
}clamp() scales type relative to the viewport. Container queries make decisions relative to the component itself. They work together — clamp() handles the smooth scaling, container queries handle the contextual adjustments.
12 - Footer
footer p {
font-family: var(--font-ui);
font-size: var(--step-1);
max-width: none;
margin-bottom: 0;
}Footer text sits at --step-1 — readable but clearly subordinate to the main content.
Locked Message
