Halvor William Sanden

Web typography: clamp() with vw needs calc()

Setting the font-size like clamp(1.875rem, 3vw, 3.75rem) defeats the purpose of using rem. But if we throw calc() into the mix, we’re back in the business of user-friendly sizing.

The clamp() function sets three sizes, a min size, an in-betweeen size and a max size. Many examples use the following:

h1 {
	font-size: clamp(1.875rem, 3vw, 3.75rem);
}

The problem comes form using vw in the second argument. It makes the in-between size relative to the viewport and not to the user’s settings. It leads to a shift and cancelling of the scaling. It also grows or shrinks too rapidly or too slow.

In a 1000 px wide viewport, 3 vw is 30 px. Which is 1.875 rem when the user has the font-size settings at 100 %. The size will increase and reach its max at a 2000 px wide viewport. That’s when 3 vw equals 3.75 rem, which is 60 px at 100 %.

This system falls apart as soon as the user’s font setting is anything but 100 %. Which we can see in a Codepen example with and without calc().

At 150 %, the clamp function with vw units has yet to start increasing in size.

Same amount of pixels is the wrong size #

At 150 %, the 1.875 rem minimum clamp is 45 px. Which means the letters won't get any bigger until 3 vw equals 45 px. That happens at 1500 px.

In this case, users with settings at 100 % and 150 % get the same pixel font size on viewports between 1500 and 2000 px. Tangents like these means that the scaling is delayed or premature, and it defeats the purpose of using rems in the first place. In reality, the relative sizes are 1.875 rem for users at 150 % and 2.8125 rem for those at 100 %.

The max size isn’t reached until the viewport is 3000 px wide. Which is still fairly wide for most people. Settings above 100 % doesn’t mean bigger screens. It means that the user wants or needs bigger letters.

For users with smaller font settings, it’s the other way around. At 75 %, 1.875 rem is 22.5 px. This means that the font size starts increasing at 750 px wide viewports and reaches max at 1500 px.

Users are not viewports #

We can make the in-between value relative to the users’ settings again by using calc() and adding rem to the mix.

h1 {
	font-size: clamp(1.875rem, calc(1.875rem + 1vw), 3.75rem);
}

This makes the value relative to the user settings and slightly relative to the viewport. The vw value should be smaller than if it was used alone because now we have a rem base. It also renders the minimum size somewhat useless, because the user will never get below the rem size inside calc anyway. We can set the second argument’s rem value slightly lower than the first, but it might be unnecessary in most cases since we’re mostly talking about just a few pixels. I recommend testing and adjusting.

If we want to make the second rem value systematically smaller, we can calculate how many pixels the vw value equals at a small viewport, like 400 px. Which is 4 px or 0.25 rem. And subtract that from the rem value.

h1 {
	font-size: clamp(1.875rem, calc(1.625rem + 1vw), 3.75rem);
}

For users with not 100 % font size, that number will be different but, as mentioned, we’re usually just talking about a few pixels.

Reusability and fallback #

The calc() function is close to what I used before clamp() was available. The biggest difference being that I used vmin instead of vw to limit the scaling somewhat.

I’m still using it as a fallback, but for reusability’s sake, I use vw and define the functions as variables, or custom properties. I’m not concerned with perfect fallbacks and similarity across browsers; I don’t see the point in pixel-perfecting something that works so poorly with px units.

--txtl1-fb: calc(1.875rem + 1vw);
--txtl1: clamp(1.875rem, var(--txtl1-fb), 3.75rem);

h1 {
	font-size: var(--txtl1-fb);
	font-size: var(--txtl1);
}

A more realistic scale #

The example values above are used for their simplicity in order to demonstrate conversions. For a more complete and realistic example, we can go a bit below a fixed size for the min value, a bit lower again for the in-between size plus a small vw increase. For the max size, we find it by testing and adjusting. Generally speaking, the bigger the initial font size is, the more vw we can add and the bigger our max can be.

As for any magic ratios, there are none. Some kind of scale can be a good start, but we get the best result by using our eyes and the dev tools.

--txtl1: clamp(1.55rem, calc(1.45rem + 1.3vw), 2.3rem);
--txtl2: clamp(1.4rem, calc(1.3rem + 1vw), 1.9rem);
--txtl3: clamp(1.17rem, calc(1rem + 0.75vw), 1.48rem);
--txtbody: clamp(1rem, calc(0.9rem + 0.35vw), 1.2rem);
--txtsmall: clamp(0.8rem, calc(0.75rem + 0.35vw), 0.8rem);