Type something to search...
What is fluid typography and how do you build it with clamp()?

What is fluid typography and how do you build it with clamp()?

Fluid typography is an approach where font sizes grow and shrink smoothly with the width of the viewport, instead of jumping between fixed sizes at media query breakpoints. In CSS you build it with the clamp() function, which takes three values: a minimum, a preferred value and a maximum. The preferred value usually mixes a rem amount with a viewport unit such as vw, so the text scales as the screen widens, while the minimum and maximum stop it from becoming too small on phones or absurdly large on wide monitors. A typical rule looks like font-size: clamp(1rem, 0.9rem + 0.5vw, 1.25rem). One line of CSS replaces several breakpoints and gives you a size that suits every screen in between.

Fixed breakpoints leave awkward gaps: a heading that looks right at 768 pixels can look cramped at 760 and oversized at 1000. Fluid type removes those gaps and usually reduces the amount of CSS you maintain. In this article you'll learn how clamp() works, how to calculate the slope and intercept for any pair of sizes, how to build a full fluid type scale, how to keep fluid text accessible when users zoom, and when fluid sizing is the wrong tool.

How clamp() Works

clamp() is a CSS math function that returns a value bounded by a lower and upper limit. Its syntax is:

clamp(MIN, PREFERRED, MAX)

The browser calculates the preferred value first. If the result is smaller than the minimum, it uses the minimum. If it's larger than the maximum, it uses the maximum. Otherwise it uses the preferred value as is. It behaves exactly like max(MIN, min(PREFERRED, MAX)).

Here's a simple heading rule:

h1 {
  font-size: clamp(2rem, 1.5rem + 2.5vw, 3.5rem);
}

With a 16 pixel root font size, this heading:

  • Never drops below 32px: the 2rem minimum applies on small screens.
  • Grows with the viewport: between the limits, it's 24px plus 2.5% of the viewport width.
  • Stops at 56px: the 3.5rem maximum caps it on large screens.

clamp() is supported in all current versions of Chrome, Edge, Firefox and Safari, and has been since 2020, so you can use it without a fallback for nearly all audiences.

Why Not Use vw on Its Own?

You might wonder why you can't just write font-size: 4vw. There are two problems. First, pure viewport units have no limits, so text becomes unreadably small on narrow screens and huge on ultra-wide displays. Second, and more seriously, text sized only in vw doesn't respond to browser zoom in the way users expect, because zooming changes the size of a CSS pixel but the viewport width in CSS pixels shrinks at the same rate. Adding a rem component to the preferred value fixes most of this, which is why fluid formulas almost always look like rem + vw rather than vw alone.

The Maths Behind a Fluid Formula

The preferred value in a fluid rule is a straight line. Font size goes up by a fixed amount for every pixel of viewport width. To build one, you choose four numbers:

  • Minimum font size: the size at your smallest target viewport.
  • Maximum font size: the size at your largest target viewport.
  • Minimum viewport width: where scaling starts.
  • Maximum viewport width: where scaling stops.

From those you calculate a slope and an intercept.

Step-by-Step Calculation

Say you want body text to be 16px at a 360px viewport and 20px at a 1280px viewport.

  1. Find the slope: divide the change in font size by the change in viewport width. (20 − 16) ÷ (1280 − 360) = 4 ÷ 920 = 0.004348.
  2. Convert the slope to vw: multiply by 100, because 1vw is 1% of the viewport. 0.004348 × 100 = 0.4348vw.
  3. Find the intercept: subtract the slope multiplied by the minimum viewport from the minimum size. 16 − (0.004348 × 360) = 16 − 1.565 = 14.435px.
  4. Convert the intercept to rem: divide by 16. 14.435 ÷ 16 = 0.9022rem.
  5. Convert the limits to rem: 16px is 1rem and 20px is 1.25rem.

Put it together:

body {
  font-size: clamp(1rem, 0.9022rem + 0.4348vw, 1.25rem);
}

You can check the result at each end. At 360px wide, 0.4348vw is 1.565px, and 14.435 + 1.565 = 16px. At 1280px, 0.4348vw is 5.565px, and 14.435 + 5.565 = 20px. Between those widths the size rises evenly.

A Small Calculator Function

Doing this by hand gets tedious once you have a full scale. This JavaScript function produces the clamp() string for you:

function fluidClamp(minSize, maxSize, minVw = 360, maxVw = 1280, root = 16) {
  const slope = (maxSize - minSize) / (maxVw - minVw);
  const intercept = minSize - slope * minVw;
  const round = (n) => Number(n.toFixed(4));

  const min = round(minSize / root);
  const max = round(maxSize / root);
  const base = round(intercept / root);
  const vw = round(slope * 100);

  return `clamp(${min}rem, ${base}rem + ${vw}vw, ${max}rem)`;
}

console.log(fluidClamp(16, 20));
console.log(fluidClamp(28, 48));
clamp(1rem, 0.9022rem + 0.4348vw, 1.25rem)
clamp(1.75rem, 1.2609rem + 2.1739vw, 3rem)

Run it in your browser console or with Node, paste the output into your stylesheet, and you're done.

Building a Fluid Type Scale

Most sites need more than one fluid size. A clean way to manage them is with custom properties, one per step of your scale. Define them once on :root, then use them everywhere.

:root {
  --step--1: clamp(0.8333rem, 0.7681rem + 0.2899vw, 1rem);
  --step-0:  clamp(1rem, 0.9022rem + 0.4348vw, 1.25rem);
  --step-1:  clamp(1.2rem, 1.0582rem + 0.6304vw, 1.5625rem);
  --step-2:  clamp(1.44rem, 1.2392rem + 0.8924vw, 1.9531rem);
  --step-3:  clamp(1.728rem, 1.4488rem + 1.2407vw, 2.4414rem);
  --step-4:  clamp(2.0736rem, 1.6908rem + 1.7011vw, 3.0518rem);
}

body { font-size: var(--step-0); }
h1   { font-size: var(--step-4); }
h2   { font-size: var(--step-3); }
h3   { font-size: var(--step-2); }
h4   { font-size: var(--step-1); }
small, .caption { font-size: var(--step--1); }

This scale uses a ratio of 1.2 at the smallest viewport and 1.25 at the largest. That's a useful trick: headings grow faster than body text, so the contrast between levels increases on bigger screens where there's room for it, and stays compact on phones. If you want to understand how to choose the ratio itself, see what a modular type scale is and how to create one.

You can generate those values with the same calculator by looping over the steps:

const steps = [-1, 0, 1, 2, 3, 4];
const minBase = 16, minRatio = 1.2;
const maxBase = 20, maxRatio = 1.25;

for (const step of steps) {
  const min = minBase * minRatio ** step;
  const max = maxBase * maxRatio ** step;
  console.log(`--step-${step}: ${fluidClamp(min, max)};`);
}

Fluid Line Height and Spacing

Font size isn't the only thing that can be fluid. Line height usually works best as a unitless number, which scales automatically with the font size, so you rarely need clamp() for it. Spacing is different. Margins between sections often benefit from the same treatment:

:root {
  --space-s: clamp(1rem, 0.9022rem + 0.4348vw, 1.25rem);
  --space-l: clamp(2rem, 1.6087rem + 1.7391vw, 3rem);
}

h2 {
  font-size: var(--step-3);
  line-height: 1.2;
  margin-block: var(--space-l) var(--space-s);
}

Tying spacing to the same viewport range as your type keeps the proportions of the page consistent at every width.

Keeping Fluid Type Accessible

Fluid typography has one real accessibility pitfall: it can stop text from scaling when the user zooms. WCAG 2.2 Success Criterion 1.4.4 (Resize Text) requires that text can be resized up to 200% without loss of content or function, and browser zoom is the most common way people do that.

How Zoom Interacts with vw

When you zoom a page to 200%, each CSS pixel becomes twice as large on screen, so the viewport becomes half as wide in CSS pixels. A rem value stays the same in CSS pixels and therefore doubles on screen. A vw value halves in CSS pixels and doubles on screen, which cancels out: it ends up roughly the same physical size. So the more of your preferred value comes from vw, the less the text grows when zoomed.

The maximum value adds another catch. If a large heading has already reached its rem maximum, zooming will scale it fully, because rem scales. But if your maximum were in px or vw, it might not.

Practical Rules

Follow these rules and fluid type will behave well under zoom:

  • Always use rem for the minimum and maximum: rem respects both zoom and the user's default font size setting.
  • Always include a rem component in the preferred value: never write clamp(1rem, 3vw, 2rem) for body text.
  • Keep the ratio between maximum and minimum modest: a maximum no more than about 2.5 times the minimum is a commonly cited guideline that keeps 200% zoom achievable.
  • Test it: zoom to 200% at a desktop width and check that body text and headings get visibly larger.

You can confirm the effective size in DevTools. In Chrome, select the element, open the Computed tab and look at font-size while you zoom.

Container Query Units for Components

vw ties text to the whole browser window. For reusable components such as cards, that's not always what you want. A card in a narrow sidebar should have smaller text than the same card in a wide main column, even though the viewport is the same.

Container query units solve this. Once an element is a size container, its children can use cqi (1% of the container's inline size) in place of vw:

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

.card h3 {
  font-size: clamp(1.125rem, 0.95rem + 2cqi, 1.75rem);
}

The maths is the same as before, except the "viewport" widths in your calculation become the narrowest and widest sizes the container will realistically have. Container query units are supported in all current major browsers.

When Fluid Typography Isn't the Right Fit

Fluid sizing is a good default for headings and display text, but it's not always necessary:

  • Body text with a small range: if your body text only moves from 16px to 18px, a single breakpoint is often just as clear and easier for colleagues to understand.
  • UI text: buttons, form labels and navigation usually look best at a fixed size, because they need to line up with icons and fixed-height controls.
  • Data tables and dashboards: users scan these by position, and text that changes size as you resize a window can be distracting.
  • Very wide ranges: a hero heading that goes from 32px to 120px will be hard to make zoom-friendly. Consider capping it lower or switching to a stepped approach above a certain width.

Mixing Fluid and Fixed Sizes

There's nothing wrong with combining the two approaches. A common pattern is fluid headings with a fixed body size and one media query for large screens:

body {
  font-size: 1rem;
}

@media (min-width: 64em) {
  body {
    font-size: 1.125rem;
  }
}

h1 { font-size: clamp(2rem, 1.2174rem + 3.4783vw, 4rem); }
h2 { font-size: clamp(1.5rem, 1.1087rem + 1.7391vw, 2.5rem); }

Notice the media query uses em, which keeps it in step with the user's font size setting.

Debugging Fluid Sizes

When a fluid size doesn't look right, check these points in order:

  1. Is the preferred value inside the limits at all? If your intercept is wrong, the preferred value may always be below the minimum, so the text never changes size.
  2. Is the root font size what you assumed? If a stylesheet sets html { font-size: 62.5%; }, then 1rem is 10px, and every value calculated with 16 will be off.
  3. Are you testing the right width? The scaling only happens between your minimum and maximum viewport widths. Resize the browser across that whole range.
  4. Is something overriding it? A more specific selector or a utility class may be setting a fixed size.

A quick way to see the effect is to drag the responsive mode width in DevTools while watching the computed font-size. It should change smoothly and stop at the limits.


FAQ: Fluid Typography

It's text that changes size gradually as the screen gets wider or narrower, rather than switching between a few fixed sizes at breakpoints. A minimum and a maximum keep it within a readable range.

Yes. clamp() works in all current versions of Chrome, Edge, Firefox and Safari, including on iOS and Android. Only very old browsers such as Internet Explorer lack support.

Use rem. Rem values respect the user's default font size setting and scale with browser zoom, while px values ignore the font size setting and can make your type less accessible.

Not by itself. It can cause problems if the preferred value relies too heavily on viewport units, because text then grows less when the page is zoomed. Keep a rem component in the preferred value and test at 200% zoom.

Often you don't need them for font sizes, because clamp() handles the transition. You may still use media queries for layout changes, or to switch a component between fixed sizes where fluid scaling isn't helpful.

A range from roughly 320 to 400 pixels at the low end up to 1200 to 1440 pixels at the high end suits most sites. Pick widths that match the narrowest phone and the widest content layout you design for.


Conclusion

Fluid typography lets text scale smoothly with the viewport, and clamp() makes it practical with a single declaration: a rem minimum, a preferred value that mixes rem and vw, and a rem maximum. Once you understand that the preferred value is a straight line defined by two points, you can calculate it for any pair of sizes, or generate a complete scale with a few lines of JavaScript.

Use it where it adds value, particularly headings and spacing, and keep accessibility in view by sticking to rem limits, including a rem component in every formula and testing at 200% zoom. Combined with custom properties, a fluid scale gives you less CSS, fewer breakpoints and type that looks considered on every screen size.

Share :

Related Posts

How to make typography accessible?

How to make typography accessible?

You make typography accessible by choosing clear typefaces, setting text in relative units so it scales with user preferences, giving it enough colou

Dive Deeper
What are the parts of a letterform?

What are the parts of a letterform?

A letterform is the shape of a single letter, and typographers break it down into named parts. The main ones are the stem (the main vertical

Dive Deeper
What are ascenders, descenders and the baseline?

What are ascenders, descenders and the baseline?

The baseline is the invisible line that letters sit on. Ascenders are the parts of lowercase letters that rise above the x-height, such as th

Dive Deeper