Fluid Type Scale in CSS: How to Build One with clamp() Step by Step

Fluid Type Scale in CSS: How to Build One with clamp() Step by Step

by | Sep 23, 2026 | Uncategorized | 0 comments

A fluid type scale built with CSS clamp() lets your typography grow smoothly between a small screen and a large screen, without a single media query. One line per size, no breakpoint jumps, no half-broken headings at 900px.

Most tutorials stop at “here is a generator, paste the output”. This one goes further: we derive the math by hand so you can adjust it, we publish a complete scale from caption to h1 with the exact CSS values, and we explain why a font size written in vw alone silently breaks browser zoom and fails WCAG.

Quick answer: the anatomy of a fluid font size

A fluid font size is a clamp() with three parts:

font-size: clamp(MIN, PREFERRED, MAX);
  • MIN: the smallest size, in rem. Applies at and below your minimum viewport.
  • PREFERRED: a rem + vw expression that interpolates linearly between the two ends.
  • MAX: the largest size, in rem. Applies at and above your maximum viewport.

Example, taken from the scale we build below:

h1 { font-size: clamp(2.488rem, 2.173rem + 1.575vw, 3.433rem); }

The critical rule: the middle value must always contain a rem component. A preferred value made only of vw is the accessibility trap we cover in detail further down. Related reading: Typescale Font Size Clamp Generator.

typography font sizes

Step 1: Define your viewport range

You need two anchor widths: the width where text stops shrinking, and the width where it stops growing.

Anchor Value in px Value in rem (px / 16)
Minimum viewport 320px 20rem
Maximum viewport 1280px 80rem

320px still covers the smallest phones in use, and 1280px is where most content containers stop mattering. If you cap your layout at 1440px or 1600px, use that instead. Just keep in mind: the wider the range, the flatter the slope, and a flat slope means the interpolation feels almost static in the middle.

Step 2: Pick a base size and two ratios

A type scale is a base size multiplied by a ratio, step after step. For a fluid scale you need two sets: one for the small end, one for the large end. Fluid vs. responsive typography with CSS clamp covers this in more depth.

  • Small screen: base 16px (1rem), ratio 1.2 (minor third). Tight ratios keep headings from eating the screen on mobile.
  • Large screen: base 18px (1.125rem), ratio 1.25 (major third). A wider ratio gives desktop headings more presence.

Using a smaller ratio on mobile and a larger one on desktop is the whole point of a fluid scale. A single ratio applied everywhere is just a resized version of the same design.

The resulting size table

Each step is base × ratio^step. Here are seven steps, from caption to h1:

Token Usage Min @320px Max @1280px
–step–1 Caption, legal, figcaption 0.833rem (13.3px) 0.9rem (14.4px)
–step-0 Body text 1rem (16px) 1.125rem (18px)
–step-1 Lead paragraph, h5 1.2rem (19.2px) 1.406rem (22.5px)
–step-2 h4 1.44rem (23px) 1.758rem (28.1px)
–step-3 h3 1.728rem (27.6px) 2.197rem (35.2px)
–step-4 h2 2.074rem (33.2px) 2.746rem (43.9px)
–step-5 h1, hero 2.488rem (39.8px) 3.433rem (54.9px)

Work in rem, not px. Everything downstream depends on it, both for the math and for the user who set their browser default font size to 20px.

typography font sizes

Step 3: Convert min and max into a clamp() formula

The preferred value is the equation of a straight line: y = slope × x + intercept, where x is the viewport width. Two formulas is all you need.

The slope

slope = (maxFontSize - minFontSize) / (maxViewport - minViewport)
slopeInVw = slope × 100

All values in rem. Multiplying by 100 turns the ratio into vw units, because 100vw equals the full viewport width. There is more on it in Creating a Fluid Type Scale with CSS Clamp.

The intercept

intercept = minFontSize - (slope × minViewport)

This is the rem anchor, and it is what keeps zoom working.

Assemble

font-size: clamp(minFontSize rem, intercept rem + slopeInVw vw, maxFontSize rem);

Step 4: A fully worked example (h1)

Let us do the h1 by hand, with the numbers from our table: min 2.488rem, max 3.433rem, viewports 20rem to 80rem.

  1. Size difference: 3.433 − 2.488 = 0.945rem
  2. Viewport difference: 80 − 20 = 60rem
  3. Slope: 0.945 / 60 = 0.01575
  4. Slope in vw: 0.01575 × 100 = 1.575vw
  5. Intercept: 2.488 − (0.01575 × 20) = 2.488 − 0.315 = 2.173rem
  6. Result: clamp(2.488rem, 2.173rem + 1.575vw, 3.433rem)

Verify both ends, it takes ten seconds:

  • At 320px: 1.575vw = 5.04px = 0.315rem, so 2.173 + 0.315 = 2.488rem. Correct.
  • At 1280px: 1.575vw = 20.16px = 1.26rem, so 2.173 + 1.26 = 3.433rem. Correct.

If your two checks land on your min and max, your formula is right. Every other step in the scale uses the same three lines of arithmetic.

typography font sizes

Step 5: The complete fluid type scale (copy and paste)

Here is the whole scale, computed with the method above, ready to drop into a design system. Range 320px to 1280px, ratios 1.2 to 1.25.

:root {
  /* Fluid type scale: 320px -> 1280px */
  --step--1: clamp(0.833rem, 0.811rem + 0.110vw, 0.9rem);
  --step-0:  clamp(1rem,     0.958rem + 0.208vw, 1.125rem);
  --step-1:  clamp(1.2rem,   1.131rem + 0.343vw, 1.406rem);
  --step-2:  clamp(1.44rem,  1.334rem + 0.530vw, 1.758rem);
  --step-3:  clamp(1.728rem, 1.572rem + 0.782vw, 2.197rem);
  --step-4:  clamp(2.074rem, 1.850rem + 1.120vw, 2.746rem);
  --step-5:  clamp(2.488rem, 2.173rem + 1.575vw, 3.433rem);
}

And the application layer. Keep tokens and usage separate, so a redesign never means a search and replace across 40 files:

html {
  /* never set a px font-size here, it overrides user preferences */
  font-size: 100%;
}

body {
  font-size: var(--step-0);
  line-height: 1.6;
}

h1 { font-size: var(--step-5); line-height: 1.1;  text-wrap: balance; }
h2 { font-size: var(--step-4); line-height: 1.15; text-wrap: balance; }
h3 { font-size: var(--step-3); line-height: 1.2;  text-wrap: balance; }
h4 { font-size: var(--step-2); line-height: 1.3; }
h5,
.lead { font-size: var(--step-1); line-height: 1.5; }

small,
figcaption,
.caption { font-size: var(--step--1); line-height: 1.4; }

p { text-wrap: pretty; max-width: 65ch; }

Bonus: a reusable formula, no calculator needed

If you would rather not recompute the slope every time a designer changes a size, let the browser do the algebra. This helper takes only the min and max, in unitless rem values:

:root {
  --vw-min: 20;  /* 320px in rem */
  --vw-max: 80;  /* 1280px in rem */
}

.fluid {
  --min: 2.488;
  --max: 3.433;
  font-size: clamp(
    var(--min) * 1rem,
    var(--min) * 1rem + (var(--max) - var(--min)) *
      ((100vw - (var(--vw-min) * 1rem)) / (var(--vw-max) - var(--vw-min))),
    var(--max) * 1rem
  );
}

It compiles to exactly the same curve, it is pure CSS, and it keeps the intent readable in code review. The trade-off is verbosity, so many teams use it during design exploration and ship the flattened values.

Why viewport units alone break accessibility zoom

This is the part generators do not explain, and it is the reason a lot of “fluid” sites fail an accessibility audit.

Suppose you skip clamp() and write your hero as pure viewport units:

/* Do not ship this */
h1 { font-size: 4.29vw; }

On a 1280px viewport that is 54.9px, exactly our max. Looks fine. Now a user with low vision presses Ctrl and + to zoom to 200%.

Browser zoom works by halving the CSS layout viewport and then scaling everything up by 2. So the viewport becomes 640 CSS pixels wide. And 4.29vw of 640px is 27.4px, which then gets rendered at 2x. The text ends up at the same physical size as before. The user zoomed and nothing got bigger.

Declaration At 100% zoom (1280px) At 200% zoom (640px layout) Effective enlargement
4.29vw 54.9px 27.4px CSS, rendered 2x 0%
clamp(2.488rem, 2.173rem + 1.575vw, 3.433rem) 54.9px 44.8px CSS, rendered 2x +63%

Two failures come from viewport-only sizing:

  1. WCAG 2.2 SC 1.4.4 (Resize Text, level AA) requires text to be resizable up to 200% without loss of content or function. Text that refuses to grow fails it.
  2. User font size preferences are ignored. A viewport unit does not care that someone set their browser default to 24px. A rem does.

The rem intercept is what fixes both. It is a fixed, preference-aware anchor that zoom multiplies, while the vw part only handles the responsive delta. That is also why the min and max bounds should never be written in px.

How to test it in two minutes

  • Zoom to 200% and 400% and confirm every text size visibly grows.
  • Set the browser default font size to Large or Very large and reload. Body text must change.
  • Check a 320px viewport: no heading should overflow or force horizontal scroll.
  • Check your widest supported screen: text must stop growing, not keep inflating.
  • Inspect the computed value in DevTools at 3 or 4 widths, not just the two anchors.

Rule of thumb: if the vw portion contributes more than roughly half of the total size at your max viewport, the slope is too aggressive and zoom will feel weak. Reduce the gap between min and max, or widen the viewport range.

typography font sizes

Beyond font size: what else should be fluid

Line height

Do not make line-height fluid with clamp(). Use unitless values and let them scale with the font size. Larger text needs tighter leading, which is why the h1 above sits at 1.1 while body text sits at 1.6.

Spacing

The same math works for margins, padding and gaps. Reuse the formula with your spacing values so rhythm stays proportional to type:

:root {
  --space-s:  clamp(0.75rem, 0.667rem + 0.417vw, 1rem);
  --space-m:  clamp(1.5rem,  1.333rem + 0.833vw, 2rem);
  --space-l:  clamp(3rem,    2.667rem + 1.667vw, 4rem);
}

Container-relative type with cqi

Viewport units know nothing about the box your component sits in. A card in a narrow sidebar gets the same heading size as the same card in a full-width hero. Container query units solve that:

.card {
  container-type: inline-size;
}

.card h3 {
  font-size: clamp(1.25rem, 1.1rem + 1.2cqi, 1.75rem);
}

1cqi is 1% of the container inline size. Support is broad across current browsers, and the same rem + unit pattern applies, so the zoom safeguard still holds. Use vw for page-level typography and cqi for reusable components.

Five mistakes we see in production code

  1. No rem in the middle value. The single most common accessibility bug in fluid typography.
  2. Setting html { font-size: 62.5% } or a px value. It overrides the user’s browser preference and makes every rem a lie.
  3. Min and max too far apart. A body text going from 14px to 24px looks broken at the midpoint. Keep body text within a 2 to 4px range.
  4. Applying the scale to every element individually. Define tokens once in :root, reference them everywhere.
  5. Forgetting the max bound. On a 3440px ultrawide monitor, an unclamped heading becomes a billboard.

FAQ

What is a fluid type scale in CSS?

It is a set of font sizes that interpolate continuously between a minimum and a maximum as the viewport changes, instead of jumping at fixed breakpoints. In modern CSS it is implemented with the clamp() function and stored as custom properties.

Is clamp() safe to use in 2026?

Yes. clamp() has been supported in every major browser since 2020 and is part of Baseline widely available. No fallback is needed for current browsers. If you support very old environments, ship a plain rem value first and let clamp() override it.

Does clamp() font sizing hurt accessibility?

Not if the preferred value contains a rem component and the min and max bounds are expressed in rem. Written that way it respects browser zoom and user font size settings. Written with vw alone, it fails WCAG SC 1.4.4.

What viewport range should I use?

320px to 1280px is a safe default. Use a wider max, such as 1440px or 1600px, if your layout container goes that wide. Avoid ranges wider than about 320px to 1920px, because the slope becomes so flat that the scaling is barely perceptible.

Should I use a calculator or compute the values myself?

Calculators are fine for the first draft. Understanding the math matters when a designer asks for one heading to be slightly larger, when you need a container-based variant, or when an audit asks you to justify your zoom behaviour. Three lines of arithmetic per step is not a heavy price for full control.

Can I use rem instead of vw for the fluid part?

No. rem does not change with viewport width, so the value would be constant and clamp() pointless. You need a viewport-relative unit (vw, vi, or cqi) for the variable part, and rem for the fixed anchor.

How many steps should a type scale have?

Seven is enough for most marketing and content sites: one below body, body, and five above. Adding more steps usually means the design lacks hierarchy rules, not sizes.

Wrapping up

A fluid type scale with clamp() is three formulas, one table of values, and a discipline: always keep a rem in the middle. Do that and you get typography that reads well from a 320px phone to a 27 inch display, with zero media queries and zero accessibility debt.

At Joxa Design we build design systems where typography, spacing and components share the same fluid logic from day one. If you would like an audit of your current type scale, or a system built from scratch, get in touch.