Guides9 min read·

Responsive font sizes with CSS clamp(), by example

How CSS clamp() makes font sizes responsive: a fluid type scale worked out at real phone, iPad and laptop widths, why rem matters for zoom, and Tailwind.

clamp(min, preferred, max) gives you the preferred value, but never less than the minimum and never more than the maximum. For font sizes, make the preferred value a rem amount plus a vw amount, like clamp(2rem, 1.25rem + 3vw, 3.5rem): the text grows with the screen, stops at sensible limits, and still gets bigger when someone zooms. Below is the arithmetic at real screen widths, a complete type scale and the Tailwind syntax.

How clamp() works

clamp() takes three values. The browser works out the middle one, then checks it against the other two:

h1 { font-size: clamp(2rem, 1.25rem + 3vw, 3.5rem); }
/*                    MIN   PREFERRED      MAX      */
  • If the preferred value is below MIN, you get MIN.
  • If it's above MAX, you get MAX.
  • Otherwise you get the preferred value.

That's the same as max(MIN, min(PREFERRED, MAX)) (MDN: clamp()). You can mix units freely, and the middle value can do maths without calc().

The three units that matter here:

  • rem: the root font size. 16px by default, but it changes when someone sets a bigger default font size in their browser.
  • vw: 1% of the viewport width. On a 402px-wide iPhone 17 Pro, 1vw is 4.02px. (The viewport is measured in CSS pixels, not the screen's physical pixels; here's the difference.)
  • px: fixed, whatever the reader's settings.

So 1.25rem + 3vw reads as "20px, plus 3% of the screen width". That's a straight line: the heading grows 3px for every 100px of extra width.

Work out the numbers from two points

You don't pick a vw value by feel. Decide what size you want on a small phone and on a laptop, and let the arithmetic write the clamp().

  1. Pick two widths. This guide uses 400px and 1200px. Every phone up to 400px wide gets the minimum: the Galaxy S25 (360), iPhone SE (375), iPhone 17e (390) and iPhone 16 (393). Every laptop at its default display scaling is 1280px or wider, so they all get the maximum. The growth happens across big phones, foldables and tablets.
  2. Pick two sizes. For the h1: 32px at 400px wide, 56px at 1200px wide.
  3. Slope: (56 − 32) ÷ (1200 − 400) = 24 ÷ 800 = 0.03. Multiply by 100 to get the vw part: 3vw.
  4. Starting value: 32 − (0.03 × 400) = 32 − 12 = 20px. Divide by 16 to get rem: 1.25rem.
  5. Min and max in rem: 32px = 2rem, 56px = 3.5rem.

Result: clamp(2rem, 1.25rem + 3vw, 3.5rem). The same recipe works for any size:

slope     = (max size − min size) / (max width − min width)
vw part   = slope × 100
rem part  = (min size − slope × min width) / 16

Check it at real widths

Here's that h1 on six real screens. The sums use the default 16px root size and a browser window as wide as the screen.

Screen Width 1.25rem + 3vw Final size
Galaxy S25 360 20 + 10.8 = 30.8 32px (minimum)
iPhone 17 Pro 402 20 + 12.06 = 32.06 32.06px
iPhone 17 Pro Max 440 20 + 13.2 = 33.2 33.2px
iPad Air 11″ 820 20 + 24.6 = 44.6 44.6px
MacBook Air 13″ 1470 20 + 44.1 = 64.1 56px (maximum)
24″ Full HD monitor 1920 20 + 57.6 = 77.6 56px (maximum)

Phones stay at a readable 32–33px, the iPad gets something in between, and the heading stops growing before it turns into a billboard on a big monitor.

A full fluid type scale

A fluid type scale is a set of these, one per text style, all growing between the same two widths. Same recipe, 400px to 1200px:

:root {
  --step-body: clamp(1rem, 0.9375rem + 0.25vw, 1.125rem); /* 16 → 18px */
  --step-h3:   clamp(1.25rem, 1.125rem + 0.5vw, 1.5rem);  /* 20 → 24px */
  --step-h2:   clamp(1.5rem, 1.125rem + 1.5vw, 2.25rem);  /* 24 → 36px */
  --step-h1:   clamp(2rem, 1.25rem + 3vw, 3.5rem);        /* 32 → 56px */
}

body { font-size: var(--step-body); line-height: 1.6; }
h1   { font-size: var(--step-h1);   line-height: 1.1; }
h2   { font-size: var(--step-h2);   line-height: 1.2; }
h3   { font-size: var(--step-h3);   line-height: 1.3; }

What each step works out to, in px:

Style 360 402 440 820 1470 1920
body 16 16.01 16.1 17.05 18 18
h3 20 20.01 20.2 22.1 24 24
h2 24 24.03 24.6 30.3 36 36
h1 32 32.06 33.2 44.6 56 56

Two things to notice:

  • Headings grow faster than body text. The h1 is twice the body size on a phone and just over three times on a laptop (56 ÷ 18). Big screens can afford the drama; phones can't.
  • Line heights have no unit. line-height: 1.1 means "1.1 times this element's font size", so it scales with the fluid size. A fixed line-height: 40px looks fine on a 32px phone heading, but on a laptop it's smaller than the 56px text and the lines overlap.

Fixed steps per breakpoint (text-4xl md:text-5xl lg:text-6xl in Tailwind: 36, 48 and 60px) work too; clamp() just removes the jumps. If you prefer steps, responsive breakpoints in 2026 covers where to put them.

Why the middle value needs rem

People make text bigger in two ways, and both are why a vw-only font size is a bad idea.

  1. Browser zoom (Ctrl or Cmd and +). Zooming makes each CSS pixel bigger, so the window becomes fewer CSS pixels wide. Text in rem or px grows. Text in vw doesn't: 3% of a narrower window shrinks by exactly as much as the zoom enlarges it.
  2. A bigger default font size in the browser settings. rem follows it. px and vw ignore it.

WCAG 1.4.4 Resize Text (level AA) says text must be resizable up to 200% without losing content, and the W3C lists vw-only font sizes as a known failure (F94).

Here's the difference on a MacBook Air 13″ at 1470px wide:

  • vw only. font-size: 3.81vw gives a 56px heading. Zoom to 200% and the window is now 735 CSS px wide, so the heading is 28 CSS px, drawn at double size: 56px on screen. Zooming did nothing.
  • rem + vw. Our h1 at 200% zoom: 20 + (3% of 735) = 42.05 CSS px, drawn at double size: 84.1px on screen, or 150% of the original 56. The rem part grows, the vw part doesn't. At 400% zoom the window is 367.5 CSS px, the preferred value drops to 31.03 and the 32px minimum takes over: 128px on screen, 229% of the original.

So the heading can reach double size, it just takes more than 200% zoom to get there. Maxwell Barvian's analysis in Smashing Magazine turns that into a simple rule: keep the maximum at or below 2.5 times the minimum, and the text can always reach 200% within the 500% zoom limit most browsers have. Our scale is well inside it: 56 ÷ 32 = 1.75 for the h1, 1.5 for the h2.

Three habits keep you safe:

  • Always put a rem part in the middle value, and write the minimum and maximum in rem too.
  • Keep the vw part modest. Big vw numbers make zoom less effective.
  • Test it: zoom to 200% on a laptop and check your body text actually got bigger.

On phones, pinch-zoom magnifies the whole page, vw text included, so this is mostly about desktop zoom and font settings.

Keep lines a readable length

Fluid font sizes don't stop lines getting too long. A paragraph stretched across a 1470px laptop window can run well past 100 characters per line, and eyes lose their place jumping back to the start. Cap the width of text blocks:

.prose { max-width: 65ch; margin-inline: auto; }

ch is the width of the "0" character in the current font (MDN: length units). Two nice side effects: the cap scales with your fluid font size, and because most letters are narrower than a zero, 65ch holds a few more than 65 characters. WCAG 1.4.8 (level AAA) sets the upper limit at 80 characters.

On phones the screen is the limit. A 360px Galaxy with 16px of padding on each side leaves a 328px column, which is narrow but readable at 16px. Don't shrink body text below 16px to squeeze more words in. The padding side of that is in mobile padding that works, and if a giant heading word still pushes past the edge, the horizontal scroll fixes cover it.

clamp() in Tailwind

For a one-off, use an arbitrary value:

<h1 class="text-[clamp(2rem,1.25rem+3vw,3.5rem)] leading-tight">…</h1>

Class names can't contain spaces. Tailwind adds the spaces around + inside math functions like clamp() when it builds your CSS, and if you'd rather be explicit, underscores become spaces: text-[clamp(2rem,1.25rem_+_3vw,3.5rem)] produces the same rule. Tailwind recognises clamp() as a length, so it generates a font-size, not a colour (Tailwind: resolving ambiguities).

For a whole scale, define it once. In Tailwind v4, --text-* variables in @theme become text-* classes, and a --line-height companion sets the line height with them (Tailwind: font-size):

@import "tailwindcss";

@theme {
  --text-body: clamp(1rem, 0.9375rem + 0.25vw, 1.125rem);
  --text-body--line-height: 1.6;
  --text-h2: clamp(1.5rem, 1.125rem + 1.5vw, 2.25rem);
  --text-h2--line-height: 1.2;
  --text-h1: clamp(2rem, 1.25rem + 3vw, 3.5rem);
  --text-h1--line-height: 1.1;
}
<h1 class="text-h1 font-bold">…</h1>
<p class="text-body max-w-[65ch]">…</p>

In Tailwind v3, the same goes in tailwind.config.js under theme.extend.fontSize, for example h1: ['clamp(2rem, 1.25rem + 3vw, 3.5rem)', { lineHeight: '1.1' }].

Does your site fit?

Paste your address (or localhost:3000). doesitfit shows it on real screen sizes at once and tells you, bluntly, where it spills.

Free, no sign-up. Or ask Claude, ChatGPT or Codex to check it.

Fluid type is easiest to check at the extremes. doesitfit loads your page on a 360px Galaxy, a 402px iPhone and a 1470px MacBook at once, and flags any screen where a heading pushes the page sideways (SPILLING) or text ends up within 8px of the edge (TIGHT).

A prompt for your AI tool

If Claude, ChatGPT, Cursor, Lovable, v0 or Bolt wrote your CSS, paste this:

Replace the fixed font sizes on this site with a fluid type scale using clamp(). The middle value must be a rem amount plus a vw amount, never vw alone, and min and max must be in rem. Grow every size between 400px and 1200px wide: body 16→18px (clamp(1rem, 0.9375rem + 0.25vw, 1.125rem)), h3 20→24px, h2 24→36px, h1 32→56px (clamp(2rem, 1.25rem + 3vw, 3.5rem)). Keep each maximum no more than 2.5 times its minimum. Use unitless line-heights, cap paragraphs at max-width: 65ch, and remove the old per-breakpoint font-size overrides for these elements. Don't change anything else. Then tell me the h1 size at 360, 402, 820 and 1470px wide.

The last line is your check: the answer should be 32, 32.06, 44.6 and 56px. For more prompts like it, see make your AI website mobile friendly.

The short version

  • clamp(MIN, PREFERRED, MAX) = the preferred value, kept between two limits.
  • Pick a size for a small phone and for a laptop, then compute the slope (vw) and starting value (rem). 400px and 1200px are good anchor widths: small phones get the minimum, every laptop gets the maximum.
  • Always mix rem into the middle value. vw alone ignores zoom and fails WCAG 1.4.4.
  • Keep each maximum at or below 2.5 times its minimum, and line heights unitless.
  • Cap text at about 65ch, and check the extremes: 360, 402, 820 and 1470px wide.

Questions people ask

What does clamp() do in CSS?

clamp(min, preferred, max) uses the preferred value but never goes below the minimum or above the maximum. For font sizes the preferred value usually mixes rem and vw, so text grows with the screen between two limits.

Is clamp() supported in all browsers?

Yes. clamp() has worked in Chrome, Edge, Firefox and Safari since 2020, and MDN lists it as Baseline widely available, so you don't need a fallback.

Should I use px or rem inside clamp()?

Use rem for the minimum, the maximum and part of the middle value. rem follows the reader's browser font-size setting and grows with zoom; px ignores the font setting, and a vw-only size doesn't grow at all when someone zooms.

How do I use clamp() for font size in Tailwind?

Use an arbitrary value such as text-[clamp(2rem,1.25rem+3vw,3.5rem)]. In Tailwind v4 you can also define --text-h1: clamp(2rem, 1.25rem + 3vw, 3.5rem) inside @theme and use a reusable text-h1 class.

What happens if the clamp() minimum is bigger than the maximum?

The minimum wins. The CSS spec says clamp() favours the min value when the two conflict, so the result is always at least the minimum.

Keep reading