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.
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.
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:
-
Calculate
PREFERREDfor the current viewport. -
If that result is
less than
MIN→ useMIN. -
If that result is
greater than
MAX→ useMAX. -
Otherwise → use
PREFERREDitself.
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:
- Is at least 36px on tiny phones (so it never disappears).
- Is at most 48px on large screens (so it doesn't dominate).
- Smoothly scales between those two on everything in between.
.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 |
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.
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) = 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:
- At 360px wide → I want the title to be 32px .
- At 1280px wide → I want the title to be 56px .
Now you have two points on a line:
(360, 32)
and
(1280, 56)
. You can solve for slope and offset:
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 */
}
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:
- utopia.fyi/type/calculator — pick a min and max font size + min and max viewport, and copy the result
- clamp.font-size.app — same idea, simpler UI
- Fluid Type Scale — generates a whole scale of headings at once
Or memorize this 4-step recipe:
- Pick two viewport widths you care about (e.g., 360 and 1280 px).
- Pick the font-size you want at each (e.g., 32 and 56 px).
-
slope (vw) =
(maxFs − minFs) ÷ (maxVw − minVw) × 100 -
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);
}
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);
}
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.
-
min(a, b)= "use whichever is smaller" — sets a ceiling -
max(a, b)= "use whichever is bigger" — sets a floor -
clamp(min, pref, max)= both at once
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:
- No cliff at 374px. Android phones with elevated "Display size" accessibility settings don't get a 24px title anymore.
- Predictable floor. No matter how narrow the viewport gets, the title is at least 36px.
- Less code. One declaration instead of four. Easier to maintain.
-
Cleaner wrap.
text-wrap: balanceevens out two-line headlines.
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:
-
What
px,rem, andvwmean and when to use each. -
The
clamp(MIN, PREFERRED, MAX)mental model. -
How to pick a slope (
vw) and offset (rem) from two design points. -
That
clamp()isn't only for fonts — use it for padding, gaps, widths. -
The
min()/max()shortcuts. - Common pitfalls (no floor, scrollbar overflow, wrong unit choice).
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.
Last updated 2026-05-07 · Comments & corrections welcome.