Fluid Font Sizing with clamp()
The Problem
Responsive typography traditionally requires multiple media queries to adjust font sizes at different breakpoints. This creates abrupt jumps between sizes and produces verbose, hard-to-maintain CSS. AI agents frequently generate fixed px or rem values at arbitrary breakpoints instead of using fluid scaling, resulting in text that is either too small on mobile or too large on desktop, with jarring transitions between breakpoints.
The Solution
The CSS clamp() function enables fluid typography in a single line, smoothly scaling font sizes between a minimum and maximum based on viewport width. The syntax is clamp(min, preferred, max), where the preferred value typically combines a rem base with a vw component for viewport-relative scaling.
The Formula
The preferred (middle) value in clamp() should be calculated as a linear function of viewport width. The general formula is:
preferred = base-rem + (slope × 1vw)Where the slope is derived from the desired font-size range and viewport range:
slope = (max-size - min-size) / (max-viewport - min-viewport)Code Examples
Basic Fluid Typography
/* Body text: scales from 16px to 20px between 320px and 1200px viewports */
body {
font-size: clamp(1rem, 0.909rem + 0.45vw, 1.25rem);
}
/* h1: scales from 28px to 48px */
h1 {
font-size: clamp(1.75rem, 1.295rem + 2.27vw, 3rem);
}
/* h2: scales from 24px to 36px */
h2 {
font-size: clamp(1.5rem, 1.227rem + 1.36vw, 2.25rem);
}
/* h3: scales from 20px to 28px */
h3 {
font-size: clamp(1.25rem, 1.068rem + 0.91vw, 1.75rem);
}Fluid Type Scale System
:root {
/* Viewport range: 320px to 1200px */
--step--1: clamp(0.833rem, 0.787rem + 0.23vw, 1rem);
--step-0: clamp(1rem, 0.909rem + 0.45vw, 1.25rem);
--step-1: clamp(1.2rem, 1.042rem + 0.79vw, 1.563rem);
--step-2: clamp(1.44rem, 1.186rem + 1.27vw, 1.953rem);
--step-3: clamp(1.728rem, 1.339rem + 1.95vw, 2.441rem);
--step-4: clamp(2.074rem, 1.494rem + 2.9vw, 3.052rem);
}
body {
font-size: var(--step-0);
}
h1 {
font-size: var(--step-4);
}
h2 {
font-size: var(--step-3);
}
h3 {
font-size: var(--step-2);
}
small {
font-size: var(--step--1);
}With Container Queries
Container query units (cqi, cqw, and the rest) apply the same clamp() pattern relative to a container's size instead of the viewport. See Container Query Units for the full unit reference.
/* Fluid sizing relative to container width instead of viewport */
/* Additive: rem base + cqi term */
.card-title {
font-size: clamp(1rem, 0.5rem + 3cqi, 1.5rem);
}
/* Pure: cqi only, no rem base */
.card-title {
font-size: clamp(1rem, 5cqi, 2rem);
}| Pattern | Formula | When to use |
|---|---|---|
| Additive | clamp(1rem, 0.5rem + 3cqi, 1.5rem) | The rem base is a size floor independent of container width — text degrades gracefully even in a near-zero-width container. Use for headings that must stay legible at any container size. |
| Pure | clamp(1rem, 5cqi, 2rem) | Font size is directly proportional to the container's inline size (5cqi = 5% of it). Use when the container already has hard min/max width bounds set elsewhere in the CSS, and text should track the container's actual rendered size rather than degrade gracefully. |
For fixed-composition stages (slides, deck viewers, fixed-aspect embeds), the pure pattern is not just an option but the correct contract — including how to derive uniform legibility floors and handle the zoom trade-off: see Stage-Proportional Typography.
To promote these one-off values into a component-scoped type and spacing token scale — and for the three rules that keep such a scale working (rem-dominant clamps for text-zoom accessibility, container-scoped consumption, fixed-px hairlines) — see Container Query Units as a Token Scale.
Common AI Mistakes
Using fixed
pxorremvalues with media query breakpoints instead ofclamp(), creating abrupt size jumpsOmitting the
remcomponent in the preferred value (using onlyvw), which breaks zooming and accessibilitySetting minimum values too small (below
1rem/ 16px for body text), making text unreadable on mobileNot testing the formula at extreme viewport widths, leading to absurdly large text on ultrawide monitors
Using
calc()withvwalone (e.g.,calc(1rem + 1vw)) without upper or lower bounds, whichclamp()naturally providesApplying fluid sizing to every text element when only headings and display text benefit from it — body text often works fine at a fixed
1remRe-deriving a
cqicoefficient from an old viewport-breakpoint font-size when migrating to container-query sizing, without checking whether that breakpoint value was correctly sized in the first place — a formula can be mathematically continuous across container widths and still render visibly wrong against the design's stated intent
When to Use
Headings and display text that need to scale between mobile and desktop
Type scale systems where every step should fluidly adjust
Hero sections with large text that needs to shrink on small screens
Any scenario where breakpoint-based font-size changes cause visible jumps
Avoid using clamp() for:
Body text where
1remis perfectly adequate at all sizesText inside components with fixed dimensions
Situations where precise control at specific breakpoints is required