Why this tutorial exists

If you've ever written CSS like this:

.hero-title { font-size: 3rem; }
@media (max-width: 768px) { .hero-title { font-size: 2.5rem; } }
@media (max-width: 374px) { .hero-title { font-size: 1.5rem; } }

…you know the pain. You add a breakpoint, the design jumps at exactly that width, and a user with a slightly different phone size gets either a giant or microscopic title.

The CSS function clamp() replaces that whole pile of @media rules with one line that scales smoothly between two limits. By the end of this tutorial you'll be able to read, write, and debug fluid CSS confidently.

Who this is for: CSS beginners who already know the basics ( font-size , padding , rem , px ) but haven't yet used viewport units ( vw ) or the clamp() / min() / max() functions.

1. Quick refresher: px, rem, vw

Before we touch clamp() , let's make sure these three units are crystal clear. The whole point of fluid CSS is mixing them.

Unit What it means Example
px An absolute pixel. Always the same size on a given device. 16px = 16 CSS pixels
rem R oot em . Multiplies the <html> element's font-size. Browsers default to 16px, so 1rem ≈ 16px . Respects user font-size preferences. 1.5rem = 24px (default)
vw V iewport w idth percent. 1vw = 1% of the browser window's width. Changes when the user resizes. On a 400px-wide screen, 5vw = 20px

Why does rem exist if we have px ? Because some users increase their browser's default font size for readability. rem respects that; px ignores it. Use rem for typography by default.

Why does vw exist? Because we want things to grow when the screen grows. A title that's 48px on a desktop usually feels too big on a 360px phone — and vice versa, a title that's 24px on phone looks lost on a 27" monitor.

Try it yourself: Open DevTools, set font-size: 5vw on any element, then drag the window narrower and wider. The text breathes with the viewport. The problem? It can become unreadably tiny on phones, or absurdly huge on TVs. That's exactly what clamp() solves.

2. What is clamp() ?

The syntax is just three values:

clamp(MIN, PREFERRED, MAX)

The browser computes the value at runtime by following this rule:

  1. Calculate PREFERRED for the current viewport.
  2. If that result is less than MIN → use MIN .
  3. If that result is greater than MAX → use MAX .
  4. Otherwise → use PREFERRED itself.

That's the whole concept. In JavaScript pseudocode:

function clamp(min, preferred, max) {
  return Math.min(max, Math.max(min, preferred));
}

The clever part is that PREFERRED can use viewport-relative units like vw . That means it varies as the screen changes — but the MIN and MAX guardrails keep it sensible.

3. Your first fluid headline

Let's build a real example. We want a headline that:

.hero-title {
  font-size: clamp(2.25rem, 8vw + 0.5rem, 3rem);
}

Let's decode each piece:

Token What it is Resolves to
2.25rem The floor (MIN). Title can never go smaller. 2.25 × 16 = 36 px
8vw + 0.5rem The preferred value. 8% of viewport width plus a small constant offset. varies (see table below)
3rem The ceiling (MAX). Title can never go bigger. 3 × 16 = 48 px

And this is what happens at different screen widths:

Viewport width 8vw + 0.5rem Preferred After clamp() Why
320 px 25.6 px 33.6 px 33.6 px 36 px (floor) Preferred < MIN → clamped up
393 px (Pixel default) 31.4 px 39.4 px 39.4 px 39.4 px Inside the band
430 px (iPhone Pro Max) 34.4 px 42.4 px 42.4 px 42.4 px Inside the band
600 px (small tablet) 48 px 56 px 56 px 48 px (ceiling) Preferred > MAX → clamped down
1440 px (desktop) 115.2 px 123.2 px 123.2 px 48 px Way over MAX → ceiling
Live demo — resize your browser

Fluid CSS with clamp() A Beginner's Guide

Drag your browser window from very narrow to wide. The headline above grows continuously until it hits 48px, then stays there. No jumps, no breakpoints.

Verify it yourself: Open DevTools, select the headline, and run this in the Console:
getComputedStyle(document.querySelector('.demo-headline')).fontSize
Resize the window and re-run. The number changes smoothly.

4. The math behind vw + rem

Why combine vw with rem ? Because plain vw alone gives you a perfectly straight line through zero — which usually scales too steeply.

Mixing vw (the slope) with rem (the constant offset) lets you define any linear relationship between viewport width and font size. It's the equation of a line:

font-size(width)  =  slope × width  +  offset
font-size(width)  =  A × vw  +  B × rem

The two pieces work together: vw gives you scaling, rem shifts the line up or down so it passes through your two design targets.

Picking your two targets

You typically design at two breakpoints — say "phone at 360px" and "desktop at 1280px" — and decide what the font-size should be at each:

Now you have two points on a line: (360, 32) and (1280, 56) . You can solve for slope and offset:

slope = (56 − 32) / (1280 − 360)  =  24 / 920  ≈  0.0261 px per px
As vw (1vw = 1% of width):   0.0261 × 100  =  2.61vw
offset =  32 − (2.61 × 360 / 100)  =  32 − 9.4  =  22.6 px
As rem (16px):   22.6 / 16  ≈  1.41rem

So the preferred value is 2.61vw + 1.41rem . Wrap with sensible guardrails:

.title {
  font-size: clamp(2rem, 2.61vw + 1.41rem, 3.5rem);
  /*           ↑ 32px  ↑ scales smoothly  ↑ 56px */
}
Don't worry about being exact. The numbers don't need to be precise to two decimals. Round to 2.5vw + 1.4rem and the design will look identical to the eye. Use round numbers when you can.

5. Quick formula & calculator

If math isn't your favorite thing, use one of these tools — they generate the clamp() string from your two design points:

Or memorize this 4-step recipe:

  1. Pick two viewport widths you care about (e.g., 360 and 1280 px).
  2. Pick the font-size you want at each (e.g., 32 and 56 px).
  3. slope (vw) = (maxFs − minFs) ÷ (maxVw − minVw) × 100
  4. offset (rem) = (minFs − slope × minVw / 100) ÷ 16

Then write:

clamp({minFs}rem ÷ 16, {slope}vw + {offset}rem, {maxFs}rem ÷ 16)

6. Use clamp() for more than fonts

Anywhere a CSS length is valid, you can use clamp() . That includes padding , margin , gap , width , height , border-radius , even line-height .

Fluid padding

Cards that have tight padding on phone and roomy padding on desktop:

.card {
  padding: clamp(1rem, 3vw, 2.5rem);
}
Demo: padding scales with viewport
This box's padding grows as you widen the page.

Fluid gap in a stack

Vertical spacing between cards that grows with the screen:

.stack {
  display: flex;
  flex-direction: column;
  gap: clamp(0.5rem, 2vw, 2rem);
}
Demo: gap scales with viewport
Card one
Card two
Card three

Fluid container width

A content column that's full-width on phones, capped on desktops:

.container {
  width: clamp(20rem, 90vw, 64rem);
  margin: 0 auto;
}

On a 360px viewport: width = 90% of 360 = 324px (above the 20rem floor, so used as-is).
On a 1920px monitor: width = 90% of 1920 = 1728px → exceeds 64rem (1024px) → clamped to 1024px.

Fluid line-height

Tighter lines on huge headings, looser on body copy. line-height is unitless but you can still feed it through clamp() when paired with units:

h1 { line-height: clamp(1.05, 1.05 + 0.5vw, 1.2); }

7. Cousins: min() and max()

clamp() has two simpler siblings. They take any number of arguments and pick either the smallest or the largest result. They're handy when you only need one bound, not two.

min() — pick the smallest

.modal {
  width: min(90vw, 600px);
}

On phones, 90vw is smaller → modal is 90% of width. On desktops, 600px is smaller → modal is capped at 600px. Net effect: a modal that fills phones but stays readable on big screens.

max() — pick the largest

.touch-target {
  min-height: max(44px, 3vw);
}

Always at least 44px (Apple's recommended tap target), but bigger on huge screens. Either way, it's never smaller than 44px.

Mental model:

In fact, clamp(MIN, PREF, MAX) is exactly equivalent to max(MIN, min(PREF, MAX)) . The browser does that internally.

8. Common pitfalls

Pitfall 1: Forgetting the floor

/* DON'T: title can shrink to 0 on a 0-wide window */
.hero { font-size: 5vw; }

/* DO: */
.hero { font-size: clamp(1.5rem, 5vw, 3rem); }

Pure vw with no floor means tiny text on tiny screens — and accessibility problems for users who scaled up their system font.

Pitfall 2: Pure px in clamp()

/* WORKS BUT NOT IDEAL */
font-size: clamp(20px, 4vw, 48px);

/* BETTER */
font-size: clamp(1.25rem, 4vw, 3rem);

rem respects users who set a larger default browser font (an accessibility feature). px ignores them. For typography, prefer rem .

Pitfall 3: Scrollbars and vw

On Windows desktop browsers, the vertical scrollbar takes ~15px out of the visible area, but 100vw is calculated as if the scrollbar weren't there. This rarely matters for font-size, but if you set width: 100vw on a full-bleed banner, you'll get horizontal overflow. Use width: 100% for that, not 100vw .

Pitfall 4: min and max in the wrong order

/* WRONG: min is bigger than max — undefined behavior */
font-size: clamp(3rem, 5vw, 1rem);

Always make sure the first argument is smaller than the third. The browser will technically accept it, but the result is implementation-defined and hard to debug.

Pitfall 5: Negative values

clamp() works fine with negative numbers (e.g., negative margins for overflow-hugging effects), but mind the order: clamp(-2rem, -1vw, 0rem) is valid; clamp(0rem, -1vw, -2rem) is not.

9. A real-world refactor

Here's a typical "before" snippet you might see in a codebase:

/* Before: 4 rules, 3 breakpoints, jumpy */
.hero-title {
  font-size: 3rem;
  line-height: 1.1;
}

@media (max-width: 1200px) {
  .hero-title { font-size: 2.5rem; }
}

@media (max-width: 768px) {
  .hero-title { font-size: 3.2rem; } /* note: bigger than tablet — typical bug */
}

@media (max-width: 374px) {
  .hero-title { font-size: 1.5rem; } /* cliff at exactly 374px */
}

The same intent, refactored:

/* After: 1 rule, 0 breakpoints, smooth */
.hero-title {
  font-size: clamp(2.25rem, 8vw + 0.5rem, 3rem);
  line-height: 1.1;
  text-wrap: balance;
}

What we gained:

When you should still use media queries: clamp() is for single-property scaling. If you need to change layout — switch a sidebar from beside the content to below it, hide a navigation, swap grid columns — that's still a job for @media . The two tools complement each other.

10. Cheat sheet

Goal Snippet
Fluid headline (36–48 px) font-size: clamp(2.25rem, 8vw + 0.5rem, 3rem);
Fluid body text (16–18 px) font-size: clamp(1rem, 0.95rem + 0.25vw, 1.125rem);
Fluid section padding padding: clamp(2rem, 6vw, 5rem) clamp(1rem, 4vw, 3rem);
Fluid gap gap: clamp(0.5rem, 2vw, 2rem);
Phone-full / desktop-capped width width: min(90vw, 64rem);
Always-tappable button min-height: max(44px, 2.75rem);
Fluid border radius border-radius: clamp(4px, 1vw, 16px);

Browser support

clamp() , min() , and max() are supported in every modern browser shipped after early 2020 — Chrome 79+, Edge 79+, Firefox 75+, Safari 13.1+. You don't need a fallback unless you support IE11 (which has been retired since 2022).

Quick test

Drop this into any page's CSS to see clamp in action immediately:

body { font-size: clamp(1rem, 0.5rem + 1vw, 1.5rem); }

Resize your browser. The body text grows from 16px to 24px smoothly. That's it — that's the whole technique.

Wrapping up

You now know:

Next time you find yourself adding "yet another media query" to fix a font that looks too big on a tablet, stop and reach for clamp() instead. Your future self — and every visitor on a non-standard screen size — will thank you.

Further reading:

Last updated 2026-05-07 · Comments & corrections welcome.