Responsive Typography with CSS Clamp: A Practical Guide
Typography is the foundation of web design. Get it right and your site feels polished and readable across every screen. Get it wrong and you end up with text that is either too small on mobile or absurdly large on desktop — or a mess of media queries that nobody wants to maintain.
CSS clamp() solves this elegantly. One line of CSS gives you a font size that scales fluidly between a minimum and maximum value, adapting to the viewport width without a single media query. We use it on every project at Threshline, and after a few early mistakes, we have landed on an approach that is both practical and maintainable.
How clamp() works
The clamp() function takes three arguments:
font-size: clamp(minimum, preferred, maximum);
- minimum — the font size will never go below this value, regardless of viewport width.
- preferred — a fluid value (usually viewport-based) that the browser uses when it falls between the minimum and maximum.
- maximum — the font size will never exceed this value.
A simple example:
h1 {
font-size: clamp(1.75rem, 4vw, 3rem);
}
On a narrow viewport (say, 375px wide), 4vw equals 15px. Since the minimum is 1.75rem (28px at default browser font size), the browser uses 28px. On a wide viewport (1440px), 4vw equals 57.6px. Since the maximum is 3rem (48px), the browser caps it at 48px. In between, the font size scales linearly.
The result: fluid, responsive typography in a single declaration.
The problem with naive viewport units
Before clamp(), people used vw directly:
/* Don't do this */
h1 {
font-size: 5vw;
}
This creates two problems. First, on very small screens, the text becomes unreadably small. On a 320px screen, 5vw is just 16px — fine for body text but far too small for a heading. Second, on very large screens, the text becomes enormous. On a 2560px ultrawide monitor, 5vw is 128px. That is not typography. That is a billboard.
clamp() solves both extremes by setting hard floors and ceilings. But the preferred (middle) value still matters — get it wrong and the scaling feels either too aggressive or too subtle.
The formula for the preferred value
The preferred value determines how quickly the font scales between your min and max. Using a raw vw value works, but choosing the right one requires either trial-and-error or math. Here is the math.
You want a font size that equals your minimum at a small viewport and your maximum at a large viewport. Let us say:
- Minimum font size: 1.25rem (20px) at viewport width 400px
- Maximum font size: 2rem (32px) at viewport width 1200px
The preferred value should be a linear function of viewport width that produces exactly the minimum at 400px and exactly the maximum at 1200px.
The formula:
preferred = (max - min) / (maxVw - minVw) * 100vw + offset
Where:
slope = (32 - 20) / (1200 - 400) = 12 / 800 = 0.015
preferred = 0.015 * 100vw = 1.5vw
offset = min - slope * minVw
= 20 - 0.015 * 400
= 20 - 6
= 14px = 0.875rem
So the declaration becomes:
h2 {
font-size: clamp(1.25rem, 0.875rem + 1.5vw, 2rem);
}
At 400px viewport: 0.875rem + 1.5vw = 14px + 6px = 20px (matches minimum).
At 1200px viewport: 0.875rem + 1.5vw = 14px + 18px = 32px (matches maximum).
The key insight is the rem + vw combination in the preferred value. The rem component provides a stable base, and the vw component adds the fluid scaling. This is more predictable than using vw alone.

A practical type scale
Rather than calculating individual values for every text element, we define a type scale using clamp(). Here is the scale we use as a starting point for most projects:
:root {
/* Body text — minimal scaling */
--text-sm: clamp(0.8rem, 0.775rem + 0.125vw, 0.875rem);
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--text-lg: clamp(1.125rem, 1.05rem + 0.375vw, 1.25rem);
/* Headings — more aggressive scaling */
--text-xl: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem);
--text-2xl: clamp(1.5rem, 1.25rem + 1.25vw, 2rem);
--text-3xl: clamp(1.875rem, 1.5rem + 1.875vw, 2.5rem);
--text-4xl: clamp(2.25rem, 1.625rem + 3.125vw, 3.5rem);
/* Display — hero text, large statements */
--text-display: clamp(2.5rem, 1.5rem + 5vw, 5rem);
}
A few design decisions embedded in this scale:
Body text scales minimally. The difference between --text-base on mobile (16px) and desktop (18px) is just 2px. Body text needs to be readable at all sizes, and dramatic scaling makes line lengths unpredictable. A small bump on larger screens improves readability without disrupting the layout.
Headings scale moderately. --text-2xl goes from 24px to 32px — a meaningful difference that gives desktop headings more presence without making mobile headings uncomfortably large.
Display text scales aggressively. --text-display ranges from 40px to 80px. This is for hero sections and marketing pages where you want the typography to command attention on desktop but still fit comfortably on a phone screen.
Applying the scale
With the custom properties defined, applying them is straightforward:
body {
font-size: var(--text-base);
line-height: 1.6;
}
h1 { font-size: var(--text-4xl); }
h2 { font-size: var(--text-3xl); }
h3 { font-size: var(--text-2xl); }
h4 { font-size: var(--text-xl); }
.hero-title {
font-size: var(--text-display);
line-height: 1.1;
letter-spacing: -0.02em;
}
.subtitle {
font-size: var(--text-lg);
color: var(--color-text-secondary);
}
.caption {
font-size: var(--text-sm);
color: var(--color-text-muted);
}
If you have read our post on design tokens and dark mode, you will notice the same pattern: define semantic variables, use them everywhere, change them in one place. Typography tokens and color tokens work the same way and can live in the same CSS custom property system.

Line height and spacing considerations
Fluid font sizes create a challenge for line height and spacing. If your heading scales from 24px to 32px, the ideal line height is different at each end of that range.
Smaller text generally needs more generous line height for readability. Larger text can be tighter because the characters are more legible at a glance. Here is how we handle it:
/* Tighter line height for larger text */
h1, h2 {
line-height: 1.2;
}
h3, h4 {
line-height: 1.3;
}
/* More generous for body text */
p, li, blockquote {
line-height: 1.6;
}
For spacing between elements, we use em units so the space scales proportionally with the text:
h2 {
font-size: var(--text-3xl);
margin-top: 2em;
margin-bottom: 0.5em;
}
p {
font-size: var(--text-base);
margin-bottom: 1.25em;
}
Using em instead of rem for vertical spacing means the space between a heading and its content scales with the heading size. A large heading on desktop gets proportionally more space above it. This keeps the vertical rhythm feeling natural across screen sizes.
Maximum line length
Fluid typography needs a companion: constrained line length. As font size increases on wider screens, line length (the number of characters per line) can grow past the readable range. Optimal line length for body text is 50-75 characters.
.prose {
font-size: var(--text-base);
max-width: 65ch;
margin-inline: auto;
}
The ch unit is based on the width of the “0” character in the current font. 65ch gives you roughly 65 characters per line, which sits in the sweet spot for readability. This works regardless of font size because ch scales with the font.
For a full-width layout where you cannot constrain the content width, consider increasing line height on wider screens:
.full-width-content {
font-size: var(--text-base);
line-height: 1.6;
}
@media (min-width: 1200px) {
.full-width-content {
line-height: 1.75;
}
}
Yes, this uses a media query. That is fine. clamp() eliminates the need for font-size media queries, not all media queries. Use each tool where it works best.
Practical tips and gotchas
Always use rem for min and max values. If a user increases their browser’s base font size (an accessibility setting), rem values scale up accordingly. Using px for min and max would override that preference. The preferred value can include vw (it has to, for fluid scaling), but the guardrails should respect user settings.
/* Good — respects user font size preferences */
font-size: clamp(1rem, 0.875rem + 0.625vw, 1.25rem);
/* Bad — ignores user preferences at the extremes */
font-size: clamp(16px, 0.875rem + 0.625vw, 20px);
Do not over-scale body text. A 2px difference between mobile and desktop body text is plenty. Going from 16px to 22px makes the text feel cartoonishly large on desktop and undermines the content hierarchy. Headings should scale dramatically. Body text should scale subtly.
Test at the boundaries. The whole point of clamp() is that the min and max kick in at certain viewport widths. Open your browser’s responsive design mode and check what the text looks like at 320px, at your min breakpoint, at your max breakpoint, and at 2560px. The transitions at each boundary should feel natural, not abrupt.
Watch for orphans and widows. Fluid text reflows differently at every viewport width. A heading that fits on one line at 1200px might wrap to two lines at 900px, creating an awkward single-word second line. There is no pure CSS fix for this (the text-wrap: balance property helps in modern browsers), but being aware of it during QA prevents embarrassing layouts from shipping.
h1, h2, h3 {
text-wrap: balance;
}
Combine with a modular scale for consistency. Our type scale is not arbitrary — it follows a ratio. The jump from --text-base to --text-lg to --text-xl uses roughly a 1.125 (major second) ratio for body sizes and a 1.25 (major third) ratio for heading sizes. Consistent ratios create visual harmony that users feel even if they cannot articulate it.

The full implementation
Putting it all together, here is a complete responsive typography system:
:root {
/* Type scale */
--text-sm: clamp(0.8rem, 0.775rem + 0.125vw, 0.875rem);
--text-base: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
--text-lg: clamp(1.125rem, 1.05rem + 0.375vw, 1.25rem);
--text-xl: clamp(1.25rem, 1.1rem + 0.75vw, 1.5rem);
--text-2xl: clamp(1.5rem, 1.25rem + 1.25vw, 2rem);
--text-3xl: clamp(1.875rem, 1.5rem + 1.875vw, 2.5rem);
--text-4xl: clamp(2.25rem, 1.625rem + 3.125vw, 3.5rem);
--text-display: clamp(2.5rem, 1.5rem + 5vw, 5rem);
/* Font families */
--font-sans: system-ui, -apple-system, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'JetBrains Mono', 'Fira Code', ui-monospace, monospace;
}
body {
font-family: var(--font-sans);
font-size: var(--text-base);
line-height: 1.6;
-webkit-font-smoothing: antialiased;
}
h1 {
font-size: var(--text-4xl);
line-height: 1.1;
letter-spacing: -0.02em;
text-wrap: balance;
}
h2 {
font-size: var(--text-3xl);
line-height: 1.2;
letter-spacing: -0.01em;
text-wrap: balance;
}
h3 {
font-size: var(--text-2xl);
line-height: 1.3;
text-wrap: balance;
}
h4 {
font-size: var(--text-xl);
line-height: 1.4;
}
.prose {
max-width: 65ch;
}
.prose p {
margin-bottom: 1.25em;
}
.prose h2 {
margin-top: 2.5em;
margin-bottom: 0.75em;
}
.prose h3 {
margin-top: 2em;
margin-bottom: 0.5em;
}
code, pre {
font-family: var(--font-mono);
font-size: 0.9em;
}
Zero media queries for font sizes. Everything scales smoothly from 320px phones to ultrawide monitors. The guardrails ensure readability at both extremes, and the scale ratios maintain visual hierarchy at every size in between.
Beyond font size
clamp() is not limited to typography. Once you are comfortable with it, apply the same thinking to spacing, container widths, and component sizing:
:root {
--space-section: clamp(3rem, 2rem + 5vw, 8rem);
--space-card-padding: clamp(1rem, 0.75rem + 1.25vw, 2rem);
--container-width: clamp(20rem, 90vw, 75rem);
}
This creates a design system where every dimension responds fluidly to the viewport. No breakpoints, no jumps, just smooth adaptation.
Responsive typography is one of those small things that makes a massive difference in how professional a site feels. It is also one of the easiest wins — a few custom properties and you are done. If you are building a site and want help getting the typography and design system right from the start, reach out at [email protected].