Typography that works at 320px and at 1440px requires a deliberate system โ not a pile of media queries. Modern CSS gives us fluid type sizing with clamp(), container queries, and custom property cascades that eliminate most manual breakpoints for text. In this lesson you will build a complete responsive typography system using fluid type scales, viewport-relative units, and the techniques used by professional design systems at scale.
Responsive Typography Strategies
| Approach | How | Pros | Cons |
|---|---|---|---|
| Media query breakpoints | Different font-size at each breakpoint | Precise control | Verbose; sudden jumps between sizes |
| Viewport units (vw) | font-size: 4vw |
Perfectly fluid | Too small on mobile, too large on desktop without limits |
clamp(min, preferred, max) |
Fluid between hard limits | No media queries; smooth scaling; accessible | Slightly complex syntax |
| CSS custom property scale | Define scale in :root; override at breakpoints | Centralised; token-based | Requires planning upfront |
| Container queries | @container + cqi unit |
Component-relative โ not viewport-relative | Requires container context |
clamp() Fluid Type Formula
| Parameter | Meaning | Example |
|---|---|---|
| Minimum | Smallest the text will ever be (mobile) | 1rem (16px) |
| Preferred (fluid) | Viewport-relative value that scales smoothly | 2.5vw |
| Maximum | Largest the text will ever be (desktop) | 1.5rem (24px) |
| Result | clamp(1rem, 2.5vw, 1.5rem) |
16px โ 24px between ~640px and ~960px viewport |
Modular Type Scale Values
| Level | Mobile (min) | Fluid preferred | Desktop (max) |
|---|---|---|---|
| Display / h1 | 2rem (32px) | 5vw | 4rem (64px) |
| Heading / h2 | 1.5rem (24px) | 3.5vw | 2.5rem (40px) |
| Subheading / h3 | 1.25rem (20px) | 2.5vw | 1.75rem (28px) |
| Body | 1rem (16px) | 1.2vw | 1.125rem (18px) |
| Small / caption | 0.875rem (14px) | โ | 0.875rem (14px) |
clamp() should use vw or a calc expression like calc(1rem + 1.5vw). Using calc(1rem + 1.5vw) as the preferred value ensures the scaling starts from a base of 1rem and grows linearly with viewport width โ this is often smoother than a raw vw value which can be 0 at 0px viewport.clamp(1rem, calc(1rem + 1vw), 1.5rem). The calc(1rem + 1vw) middle means “start at 16px and grow 1% of viewport width per 100px”. At 400px viewport: 16 + 4 = 20px (clamped to 16px min). At 1000px: 16 + 10 = 26px. At 1600px: still 24px (clamped to max).font-size: Nvw without clamp(). At a 320px viewport, 4vw is 12.8px โ below the 16px minimum recommended for body text. At a 1920px monitor, 4vw is 76.8px โ far too large. Always wrap viewport-relative font sizes inside clamp() to enforce sensible limits.Basic Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700;800&display=swap" rel="stylesheet">
<title>Responsive Typography</title>
<style>
html { font-size: 100%; }
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
/* โโ Fluid type scale via custom properties โโ */
:root {
--text-sm: clamp(0.875rem, calc(0.875rem + 0.1vw), 0.9375rem);
--text-base: clamp(1rem, calc(1rem + 0.15vw), 1.125rem);
--text-lg: clamp(1.125rem, calc(1rem + 0.5vw), 1.375rem);
--text-xl: clamp(1.25rem, calc(1rem + 1vw), 1.75rem);
--text-2xl: clamp(1.5rem, calc(1rem + 1.75vw), 2.25rem);
--text-3xl: clamp(1.875rem, calc(1rem + 3vw), 3rem);
--text-display: clamp(2.5rem, calc(1.5rem + 5vw), 5rem);
--font-sans: 'Inter', system-ui, sans-serif;
--line-tight: 1.15;
--line-body: 1.7;
--color-ink: #0f172a;
--color-muted: #64748b;
--color-accent: #4f46e5;
}
body {
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: var(--line-body);
color: var(--color-ink);
background: #f8fafc;
}
/* โโ Page sections โโ */
.page { max-width: 1100px; margin: 0 auto; padding: 48px 24px; }
/* โโ Hero โโ */
.hero {
text-align: center;
padding: 80px 24px;
background: linear-gradient(135deg, #1e293b, #312e81);
border-radius: 20px;
margin-bottom: 64px;
}
.hero-label {
font-size: var(--text-sm);
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.1em;
color: #a5b4fc;
margin-bottom: 16px;
}
.hero-title {
font-size: var(--text-display);
font-weight: 800;
line-height: var(--line-tight);
letter-spacing: -0.03em;
color: white;
margin-bottom: 24px;
max-width: 18ch;
margin-inline: auto;
}
.hero-sub {
font-size: var(--text-lg);
line-height: 1.6;
color: #94a3b8;
max-width: 55ch;
margin-inline: auto;
}
/* โโ Prose section โโ */
.prose { max-width: 65ch; margin: 0 auto 64px; }
.prose h2 { font-size: var(--text-2xl); font-weight: 700; line-height: 1.2; letter-spacing: -0.02em; margin-bottom: 16px; }
.prose h3 { font-size: var(--text-xl); font-weight: 600; line-height: 1.3; letter-spacing: -0.01em; margin: 32px 0 12px; }
.prose p { font-size: var(--text-base); line-height: 1.75; color: #475569; margin-bottom: 16px; }
/* โโ Fluid grid of type samples โโ */
.type-samples { display: grid; grid-template-columns: repeat(auto-fill, minmax(280px, 1fr)); gap: 20px; }
.type-card { background: white; border: 1px solid #e2e8f0; border-radius: 12px; padding: 24px; }
.type-card .meta { font-size: 0.7rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--color-muted); margin-bottom: 8px; }
.type-card .sample { color: var(--color-ink); font-weight: 700; line-height: 1.2; }
</style>
</head>
<body>
<div class="page">
<section class="hero">
<p class="hero-label">Fluid Typography System</p>
<h1 class="hero-title">Type That Scales with Your Layout</h1>
<p class="hero-sub">Every size in this page is defined with clamp() โ fluid between a minimum on mobile and a maximum on desktop, with no media queries for font sizes.</p>
</section>
<div class="type-samples">
<div class="type-card"><p class="meta">--text-display</p><p class="sample" style="font-size:var(--text-display)">Aa</p></div>
<div class="type-card"><p class="meta">--text-3xl</p><p class="sample" style="font-size:var(--text-3xl)">Aa</p></div>
<div class="type-card"><p class="meta">--text-2xl</p><p class="sample" style="font-size:var(--text-2xl)">Aa</p></div>
<div class="type-card"><p class="meta">--text-xl</p><p class="sample" style="font-size:var(--text-xl)">Aa</p></div>
<div class="type-card"><p class="meta">--text-lg</p><p class="sample" style="font-size:var(--text-lg)">Aa</p></div>
<div class="type-card"><p class="meta">--text-base</p><p class="sample" style="font-size:var(--text-base)">Aa</p></div>
</div>
<section class="prose" style="margin-top:48px">
<h2>Why Fluid Type Scales Work</h2>
<p>A fluid type scale defines each size as a range โ a minimum on narrow viewports and a maximum on wide ones, with smooth linear interpolation between. The result is typography that always feels right, at any viewport width, without a single font-size media query.</p>
<h3>The clamp() Function</h3>
<p>The three arguments of clamp are minimum, preferred, and maximum. The browser uses the preferred value unless it would fall outside the min/max bounds โ at which point it clamps to the limit. Using calc(1rem + 1.5vw) as the preferred creates a smooth ramp that starts at the base rem size and grows proportionally.</p>
</section>
</div>
</body>
</html>
How It Works
Step 1 โ Custom Properties Define the Entire Scale
All seven type sizes are defined in :root as custom properties with clamp() values. Any component that needs a size uses font-size: var(--text-lg). To adjust the whole system, you change one declaration โ the property in :root.
Step 2 โ calc() Inside clamp() Creates a Linear Ramp
calc(1rem + 1.5vw) means “start at 16px and grow 1.5% of viewport width”. At 400px viewport: 16 + 6 = 22px. At 800px: 16 + 12 = 28px. The clamp() wrapping ensures this value never drops below the minimum or exceeds the maximum.
Step 3 โ max-width: 18ch on the Hero Title
Even at a 2rem display size, the title wraps at approximately 18 characters โ about 3โ4 words per line. This creates the classic “stacked headline” look from editorial design. The ch unit auto-adjusts as the font scales with the viewport.
Step 4 โ No Font-Size Media Queries Needed
Resize the window to any width โ all text sizes adjust smoothly. A 320px mobile gets minimum sizes that are readable; a 1600px widescreen gets the maximum sizes. No breakpoints, no duplicate declarations, no sudden jumps between sizes.
Step 5 โ letter-spacing Scales with Font Size via em
Using letter-spacing: -0.03em on the hero title means the tracking is always 3% of the current computed font-size. As the title scales from 40px to 80px via clamp, the letter-spacing automatically scales from -1.2px to -2.4px โ maintaining the correct visual tightness at every size.
Real-World Example: Design Token Type System
/* tokens-type.css โ production design system type tokens */
:root {
/* โโ Font families โโ */
--font-display: 'Playfair Display', Georgia, serif;
--font-body: 'Inter', system-ui, -apple-system, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Courier New', monospace;
/* โโ Fluid type scale โโ */
--text-2xs: 0.625rem;
--text-xs: 0.75rem;
--text-sm: 0.875rem;
--text-md: 1rem;
--text-lg: clamp(1.125rem, calc(1rem + 0.5vw), 1.25rem);
--text-xl: clamp(1.25rem, calc(1rem + 1vw), 1.5rem);
--text-2xl: clamp(1.5rem, calc(1rem + 2vw), 2rem);
--text-3xl: clamp(1.875rem, calc(1rem + 3vw), 2.625rem);
--text-4xl: clamp(2.25rem, calc(1rem + 4.5vw), 3.5rem);
--text-5xl: clamp(3rem, calc(1.5rem + 6vw), 5rem);
/* โโ Font weights โโ */
--weight-regular: 400;
--weight-medium: 500;
--weight-semibold: 600;
--weight-bold: 700;
--weight-extrabold: 800;
/* โโ Line heights โโ */
--leading-none: 1;
--leading-tight: 1.15;
--leading-snug: 1.3;
--leading-normal: 1.5;
--leading-relaxed: 1.625;
--leading-loose: 1.75;
/* โโ Letter spacing โโ */
--tracking-tighter: -0.05em;
--tracking-tight: -0.025em;
--tracking-normal: 0em;
--tracking-wide: 0.025em;
--tracking-wider: 0.05em;
--tracking-widest: 0.1em;
/* โโ Prose measure โโ */
--measure-narrow: 45ch;
--measure-default:65ch;
--measure-wide: 80ch;
}
/* โโ Semantic text styles โโ */
.text-display {
font-family: var(--font-display);
font-size: var(--text-5xl);
font-weight: var(--weight-bold);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-tight);
}
.text-heading-1 {
font-family: var(--font-body);
font-size: var(--text-4xl);
font-weight: var(--weight-extrabold);
line-height: var(--leading-tight);
letter-spacing: var(--tracking-tight);
}
.text-heading-2 {
font-family: var(--font-body);
font-size: var(--text-3xl);
font-weight: var(--weight-bold);
line-height: var(--leading-snug);
letter-spacing: var(--tracking-tight);
}
.text-body {
font-family: var(--font-body);
font-size: var(--text-md);
font-weight: var(--weight-regular);
line-height: var(--leading-relaxed);
}
.text-label {
font-family: var(--font-body);
font-size: var(--text-xs);
font-weight: var(--weight-semibold);
line-height: var(--leading-normal);
letter-spacing: var(--tracking-widest);
text-transform: uppercase;
}
.text-caption {
font-family: var(--font-body);
font-size: var(--text-sm);
line-height: var(--leading-normal);
color: #64748b;
}
.text-code {
font-family: var(--font-mono);
font-size: 0.875em;
line-height: var(--leading-relaxed);
}
Common Mistakes
Mistake 1 โ Unsupported clamp() usage in older builds
โ Wrong โ clamp() is not supported in IE11 or early Edge (pre-Chromium):
h1 { font-size: clamp(2rem, 5vw, 4rem); } /* no fallback for legacy browsers */
โ Correct โ provide a static fallback before the clamp() declaration:
h1 { font-size: 2.5rem; } /* fallback */
h1 { font-size: clamp(2rem, 5vw, 4rem); } /* overrides in supporting browsers */
Mistake 2 โ Using raw vw without minimum for body text
โ Wrong โ at narrow viewports, body text becomes too small to read:
p { font-size: 2vw; } /* at 400px viewport = 8px โ unreadable */
โ Correct โ always clamp viewport-relative sizes:
p { font-size: clamp(1rem, 2vw, 1.25rem); }
Mistake 3 โ Building a type system inside each component
โ Wrong โ each component defines its own font sizes in isolation:
.card-title { font-size: 1.25rem; }
.modal-title { font-size: 1.3rem; }
.sidebar-heading { font-size: 1.2rem; } /* inconsistent โ hard to maintain */
โ Correct โ define sizes once in :root tokens, reference everywhere:
:root { --text-xl: clamp(1.25rem, calc(1rem + 1vw), 1.5rem); }
.card-title, .modal-title, .sidebar-heading { font-size: var(--text-xl); }
Quick Reference
| Technique | CSS | Notes |
|---|---|---|
| Fluid heading | clamp(2rem, 5vw, 4rem) |
No font-size media queries needed |
| Fluid body | clamp(1rem, calc(1rem + 0.5vw), 1.25rem) |
Gentle scaling for comfortable reading |
| Token scale | --text-xl: clamp(...) in :root |
Single source of truth for all sizes |
| Measure | max-width: 65ch |
Optimal reading line length |
| Tight heading tracking | letter-spacing: -0.02em |
em scales with font-size |
| Viewport fallback | Static px before clamp() | Legacy browser support |