TH The Sass Way
Responsive Design

CSS clamp() Explained: Fluid Typography Without Breakpoints

CSS clamp() Explained: Fluid Typography Without Breakpoints
tldrCSS clamp() takes three values — clamp(min, preferred, max) — and returns the preferred value as long as it stays between the minimum and maximum. For fluid typography, you set a rem-based floor and ceiling with a rem + vw expression in the middle, like font-size: clamp(1rem, 0.875rem + 0.5vw, 1.25rem). Text then scales smoothly with the viewport, without media queries, while never becoming too small or too large.

What Does CSS clamp() Do?

CSS clamp() takes three values — a minimum, a preferred value, and a maximum — and returns the preferred value as long as it stays between the two limits. Written as clamp(min, preferred, max), it lets a font size (or any length) scale fluidly with the viewport while never dropping below your floor or growing past your ceiling.

That one function replaces the most common reason developers write media queries: stepping font sizes up at each breakpoint. Instead of a 16px body size that jumps to 18px at 768px and 20px at 1200px, you write a single declaration and the browser interpolates smoothly through every viewport in between.

h1 {
  font-size: clamp(2rem, 1rem + 4vw, 4rem);
}

That heading is never smaller than 2rem, never larger than 4rem, and glides between the two as the viewport changes. No breakpoints, no sudden jumps, no awkward in-between screens where the type looks wrong.

The Syntax: Minimum, Preferred, Maximum

clamp() is shorthand for a pattern you could build yourself with nested min() and max() functions. These three declarations are equivalent:

font-size: clamp(2rem, 1rem + 4vw, 4rem);
font-size: max(2rem, min(1rem + 4vw, 4rem));
font-size: min(max(2rem, 1rem + 4vw), 4rem);

The three arguments each play a distinct role:

One gotcha worth knowing: if your minimum is accidentally larger than your maximum, the browser resolves the expression in order — clamp() behaves like max(MIN, min(VAL, MAX)) — so the minimum wins. Nothing errors, your text just quietly stops scaling, which can be confusing to debug.

Building a Fluid Font Size Step by Step

The preferred value is where the real design decision lives. You are defining a straight line: at some small viewport the text should be size A, and at some large viewport it should be size B. Everything between is linear interpolation.

Say you want body text at 16px on a 400px viewport scaling to 20px on a 1200px viewport. Two steps:

1. Find the slope — how many pixels of font size you gain per pixel of viewport:

slope = (20 − 16) / (1200 − 400) = 4 / 800 = 0.005

Multiply by 100 to express it in vw: 0.5vw.

2. Find the intercept — the font size the line would have at a 0px viewport:

intercept = 16 − (0.005 × 400) = 16 − 2 = 14px = 0.875rem

Put it together:

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

Sanity check the endpoints: at 400px wide, 0.5vw is 2px, and 14 + 2 = 16px. At 1200px wide, 0.5vw is 6px, and 14 + 6 = 20px. The line passes exactly through both targets, and clamp() locks it flat outside that range.

If you'd rather not do the algebra by hand, tools exist that generate the expression from your four inputs — or you can let Sass do it, as shown below.

Why the Preferred Value Should Mix rem and vw

You will sometimes see fluid type written as pure viewport units: font-size: 3vw. Avoid this, for two reasons.

Accessibility. Users who raise their browser's default font size expect text to get bigger. A rem-based value respects that preference; a pure vw value ignores it entirely, and text-only zoom in some browsers won't scale it either. Mixing a rem term into the preferred value (0.875rem + 0.5vw) means user preferences still move the needle, and the rem-based minimum and maximum guarantee sensible bounds. When you're done, verify your text can still roughly double in size when zoomed — that's the behavior accessibility guidelines expect.

Control. A pure vw value scales from zero to infinity with nothing anchoring it to your type scale. The rem + vw form gives you an explicit line through two chosen points instead of a ratio you found by eyeballing.

If the difference between rem, em, and viewport units is still fuzzy, our guide to CSS units walks through exactly when each one earns its place.

clamp() vs min() vs max()

The three comparison functions ship together and get confused constantly. Here's the practical breakdown:

Function What it returns Typical use
min(a, b) The smaller value Cap growth: width: min(90%, 60rem)
max(a, b) The larger value Enforce a floor: font-size: max(1rem, 2vw)
clamp(min, val, max) The middle value, bounded both ways Fluid type and spacing with hard limits

The naming trips people up: min() sets a maximum (it never lets the value exceed the smaller argument), and max() sets a minimum. clamp() does both at once, which is why it's the default choice for typography.

All three accept any length-producing expression, work in nearly every property that takes a length, and are supported in all modern browsers — clamp() landed across Chrome, Edge, Firefox, and Safari in the first half of 2020, so unless you support very old browsers you can use it freely.

Let Sass Do the Math

Computing slopes and intercepts by hand gets old after the second heading level. A small Sass function turns the whole formula into one readable call:

@use "sass:math";

@function fluid($min, $max, $min-vw: 25rem, $max-vw: 75rem) {
  $slope: math.div($max - $min, $max-vw - $min-vw);
  $intercept: $min - $slope * $min-vw;
  @return clamp(#{$min}, #{$intercept} + #{$slope * 100}vw, #{$max});
}

h1 {
  font-size: fluid(2rem, 4rem);
}

Compiled with Dart Sass, that produces:

h1 {
  font-size: clamp(2rem, 1rem + 4vw, 4rem);
}

Everything stays in rem — including the viewport arguments (25rem = 400px, 75rem = 1200px at the browser default) — so the units cancel cleanly in the division. Note the math.div() call: modern Sass deprecated the / operator for division, so this is the correct, future-proof form. Drop the function in a shared partial, load it with @use, and your whole type scale becomes calls like fluid(1rem, 1.25rem) and fluid(1.5rem, 2.5rem).

The same function works for fluid spacing, too: padding: fluid(1rem, 3rem) gives sections breathing room that grows with the viewport.

When You Still Need Media Queries

clamp() removes breakpoints from typography, not from responsive design. You still want media queries when:

The winning combination in most modern codebases: clamp() for text and spacing, media or container queries for structural layout shifts. That pairing fits naturally with a mobile-first workflow — your unqueried base styles are already fluid, so the queries you do write are fewer and only handle real layout changes.

A Complete Fluid Type Scale

Here's a compact, copy-pasteable scale using custom properties, sized to run from a 400px to a 1200px viewport:

:root {
  --step-0: clamp(1rem, 0.875rem + 0.5vw, 1.25rem);  /* body: 16 → 20px */
  --step-1: clamp(1.25rem, 1rem + 1vw, 1.75rem);     /* h3: 20 → 28px */
  --step-2: clamp(1.5rem, 1.125rem + 1.5vw, 2.25rem); /* h2: 24 → 36px */
  --step-3: clamp(2rem, 1rem + 4vw, 4rem);           /* hero: 32 → 64px */
}

body { font-size: var(--step-0); }
h1   { font-size: var(--step-3); }

Two practical tips as you build your own:

Fluid typography is one piece of the larger discipline of building layouts that work everywhere — for the full picture, from viewport meta tags to responsive images, see our complete responsive design guide.

The short version to remember: clamp(min, rem + vw, max). Floor, fluid line, ceiling. Once the formula clicks, you'll wonder why you ever maintained five breakpoints just to resize an h1.

FAQ

What do the three values in clamp() mean?

The first value is the minimum (the floor your value never drops below), the second is the preferred value (usually a fluid expression like 0.875rem + 0.5vw that scales with the viewport), and the third is the maximum (the ceiling). The browser uses the preferred value whenever it falls between the two limits, and locks to the nearest limit otherwise. Inside clamp() you can write math directly without wrapping it in calc().

How do I calculate the preferred value for fluid typography?

Pick two target points, such as 16px at a 400px viewport and 20px at 1200px. The slope is the size difference divided by the viewport difference: 4 / 800 = 0.005, or 0.5vw. The intercept is the small size minus slope times the small viewport: 16 - 2 = 14px, or 0.875rem. The result is clamp(1rem, 0.875rem + 0.5vw, 1.25rem), which passes exactly through both targets.

Why should clamp() mix rem and vw instead of using vw alone?

A pure vw font size ignores the user's browser font-size preference and may not respond to text-only zoom, which is an accessibility problem. Adding a rem term to the preferred value keeps user preferences in play, and rem-based minimum and maximum values guarantee sensible bounds. It also gives you precise control: the rem + vw form defines an exact line through two chosen sizes rather than an eyeballed ratio.

What is the difference between min(), max(), and clamp()?

min() returns the smaller of its arguments, so it acts as a cap — min(90%, 60rem) never exceeds 60rem. max() returns the larger, so it enforces a floor. clamp() combines both: it bounds a fluid middle value on both sides. The naming feels backwards at first — min() sets a maximum and max() sets a minimum — which is why clamp() is usually the clearest choice for typography.

Does clamp() replace media queries entirely?

No. clamp() removes the breakpoints you used purely to resize text and spacing, but media queries still handle structural changes: a sidebar collapsing, navigation becoming a hamburger menu, or preference-based queries like prefers-reduced-motion and prefers-color-scheme. Most modern codebases pair fluid clamp() values for typography with a small number of media or container queries reserved for genuine layout shifts.

Is CSS clamp() safe to use in production?

Yes. clamp(), along with min() and max(), landed in Chrome, Edge, Firefox, and Safari during the first half of 2020, so every modern browser supports it. Unless you must support very old browsers, you can use it freely for font sizes, padding, margins, widths, and most other length-accepting properties. If you need a fallback, declare a static font-size first and let clamp() override it in supporting browsers.