Fluid Type with clamp()
Responsive typography has traditionally relied on media queries or JavaScript hacks. The CSS clamp() function offers a cleaner way: scale text linearly between minimum and maximum sizes as the viewport width changes, all in a single declaration.
clamp() takes three values:
clamp(minimum, preferred, maximum);
The function returns the preferred value, unless that value falls below the minimum (then it returns the minimum) or above the maximum (then it returns the maximum). For fluid typography, the preferred value is typically a viewport-based formula:
.banner {
width: clamp(200px, 50% + 20px, 800px); /* Yes, you can do math inside clamp()! */
}
Building a Fluid Font-Size Rule
Suppose you want an element's font-size to be 1rem at viewport widths of 360px or below, and 3.5rem at widths of 840px or above. In clamp() terms:
1rem = 360px and below
Scaled = 361px - 839px
3.5rem = 840px and above
For viewport widths between 361px and 839px, the font size must scale linearly. At 600px — halfway between 360px and 840px — the font size should be exactly halfway between 1rem and 3.5rem, i.e., 2.25rem.

The Four Steps to Linearity
This is a case of linear interpolation: deriving intermediate values between two known data points. Here's the process:
- Pick your endpoints. Choose minimum and maximum font sizes (1rem and 3.5rem) and corresponding viewport widths (360px and 840px).
- Convert widths to
rem. Assuming the default root font size of 16px, the viewport bounds become 22.5rem and 52.5rem. - Calculate the line. The pairs of (viewport width, font size) form two points on an X/Y coordinate system:

(22.5, 1) and (52.5, 3.5)The slope and Y-axis intersection determine the scaling formula:
slope = (maxFontSize - minFontSize) / (maxWidth - minWidth)
yAxisIntersection = -minWidth * slope + minFontSize
This yields a slope of 0.0833 and a Y-axis intersection of -0.875.
- Assemble the
clamp()function. The preferred value formula is:
preferredValue = yAxisIntersection[rem] + (slope * 100)[vw]
Resulting in:
.header {
font-size: clamp(1rem, -0.875rem + 8.333vw, 3.5rem);
}
The font size stops growing past 840px and stops shrinking below 360px. Everything in between scales linearly.
Accounting for Root Font-Size Changes
This approach assumes the root font size is 16px — the browser default. If a user has set their root font size to 18px, converting 360px and 840px to rem by dividing by 16 is wrong.
The only reliable fix is to calculate the values in JavaScript on page load, monitor for font-size changes, and recalculate. A utility function helps:
// Takes the viewport widths in pixels and the font sizes in rem
function clampBuilder( minWidthPx, maxWidthPx, minFontSize, maxFontSize ) {
const root = document.querySelector( "html" );
const pixelsPerRem = Number( getComputedStyle( root ).fontSize.slice( 0,-2 ) );
const minWidth = minWidthPx / pixelsPerRem;
const maxWidth = maxWidthPx / pixelsPerRem;
const slope = ( maxFontSize - minFontSize ) / ( maxWidth - minWidth );
const yAxisIntersection = -minWidth * slope + minFontSize
return `clamp( ${ minFontSize }rem, ${ yAxisIntersection }rem + ${ slope * 100 }vw, ${ maxFontSize }rem )`;
}
// clampBuilder( 360, 840, 1, 3.5 ) -> "clamp( 1rem, -0.875rem + 8.333vw, 3.5rem )"
There's no native event for root font-size changes, so you'd need a setInterval check — which comes at a performance cost. That's an extreme edge case, but it's the only way to make the approach fully responsive to user preferences.
Preventing Text Reflow
Fine control over font sizing enables a less common trick: keeping text at a constant line count across viewport widths. With a simple fluid type rule, text reflows as the width changes:


With careful sizing, text can retain the same line breaks at every viewport:


Matching Font Size to Viewport Width
The ratio between font size and viewport width must stay constant. In this example, the font goes from 1rem at 320px to 3rem at 960px:
320 / 1 = 320
960 / 3 = 320
Using the earlier clampBuilder() function:
const text = document.querySelector( "p" );
text.style.fontSize = clampBuilder( 320, 960, 1, 3 );
This alone isn't enough. You also need to set the text container's width using the ch unit — the width of the glyph "0" in the element's font. To fill the viewport horizontally, the container must be Xch wide, where X is the viewport width divided by the font's ch size at the minimum width.
This snippet measures an element's ch size:
// Returns the width, in pixels, of the "0" glyph of an element at a desired font size
function calculateCh( element, fontSize ) {
const zero = document.createElement( "span" );
zero.innerText = "0";
zero.style.position = "absolute";
zero.style.fontSize = fontSize;
element.appendChild( zero );
const chPixels = zero.getBoundingClientRect().width;
element.removeChild( zero );
return chPixels;
}
Applying the width:
function calculateCh( element, fontSize ) { ... }
const text = document.querySelector( "p" );
text.style.fontSize = clampBuilder( 320, 960, 1, 3 );
text.style.width = `${ 320 / calculateCh(text, "1rem" ) }ch`;

That introduces horizontal overflow. The problem: vw units include the width of the vertical scrollbar. The text width is calculated against the full viewport width, but the visible area is viewport width − scrollbar width, causing overflow.
You can't avoid this by using a scrollbar-exclusive metric, because the vw unit inside clamp() also accounts for the scrollbar. The font scales along the full viewport width. To prevent overflow without breaking the ratio, scale the final width down slightly:
text.style.width = `${ 320 / calculateCh(text, "1rem") }ch`;
Multiplying by 0.9 gives the text a width of 90% of the viewport, comfortably clearing the scrollbar:
function calculateCh( element, fontSize ) { ... }
const text = document.querySelector( "p" );
text.style.fontSize = clampBuilder( 20, 960, 1, 3 );
text.style.width = `${ 320 / calculateCh(text, "1rem" ) * 0.9 }ch`;

Subtracting a few pixels from the viewport width, rather than scaling, is tempting but wrong — it breaks the width-to-font ratio and reintroduces reflow:
text.style.width = `${ ( 320 - 30 ) / calculateCh( text, "1rem" ) }ch`;


Two caveats apply. First, the text width must always be a percentage of the viewport width. Second, font loading must be consistent across devices: font-family: sans-serif resolves to Arial on Chrome for Windows but Roboto on Chrome for Android, and differing glyph geometry causes reflow. Monospaced fonts yield the most predictable results.
Non-Reflowing Text Inside a Container
The technique translates to containers: apply font-size and the width rule to the parent, and let children inherit. Block-level elements like paragraphs and headings fill the container's width automatically.
There are advantages to container-level application: children resize proportionally without individual rules, and changing one element's font size in em units keeps it relative to the container.
Non-reflowing text remains finicky, but it's a subtle effect that adds polish. The same clamp() technique applies to any CSS property accepting a length unit — though often a plain font-size: 1rem is all you need. This approach is about having control available when the design calls for it.



