添加链接
link管理
链接快照平台
  • 输入网页链接,自动生成快照
  • 标签化管理网页链接

The value it returns will be the preferred value, until that preferred value is lower than the minimum value (at which point the minimum value will be returned) or higher than the maximum value (at which point the maximum will be returned).

In this example, the preferred value is 50%. On the left 50% of the 400px viewport is 200px, which is less than the 300px minimum value that gets used instead. On the right, 50% of the 1400px viewport equals 700px, which is greater than the minimum value and lower than the 800px maximum value, so it equates to 700px.

Wouldn’t it just always be the preferred value then, assuming you aren’t being weird and set it between the minimum and maximum? Well, you’re rather expected to use a formula for the preferred value, like:

.banner {
  width: clamp(200px, 50% + 20px, 800px); /* Yes, you can do math inside clamp()! */

Say you want to set an element’s minimum font-size to 1rem when the viewport width is 360px or below, and set the maximum to 3.5rem when the viewport width is 840px or above. 

In other words:

1rem   = 360px and below
Scaled = 361px - 839px
3.5rem = 840px and above

Any viewport width between 361 and 839 pixels needs a font size linearly scaled between 1 and 3.5rem. That’s actually super easy with clamp()! For example, at a viewport width of 600 pixels, halfway between 360 and 840 pixels, we would get exactly the middle value between 1 and 3.5rem, which is 2.25rem.

Pick your minimum and maximum font sizes, and your minimum and maximum viewport widths. In our example, that’s 1rem and 3.5rem for the font sizes, and 360px and 840px for the widths.

Step 2

Convert the widths to rem. Since 1rem on most browsers is 16px by default (more on that later), that’s what we’re going to use. So, now the minimum and maximum viewport widths will be 22.5rem and 52.5rem, respectively.

Step 3

Here, we’re gonna lean a bit to the math side. When paired together, the viewport widths and the font sizes make two points on an X and Y coordinate system, and those points make a line.

A two-dimensional coordinate chart with two points and a red line intersecting them.
(22.5, 1) and (52.5, 3.5)

We kinda need that line — or rather its slope and its intersection with the Y axis to be more specific. Here’s how to calculate that:

slope = (maxFontSize - minFontSize) / (maxWidth - minWidth)
yAxisIntersection = -minWidth * slope + minFontSize

That gives us a value of 0.0833 for the slope and -0.875 for the intersection at the Y axis.

Step 4

Now we build the clamp() function. The formula for the preferred value is:

preferredValue = yAxisIntersection[rem] + (slope * 100)[vw]

So the function ends up like this:

.header {
  font-size: clamp(1rem, -0.875rem + 8.333vw, 3.5rem);

You can visualize the result in the following demo:

Go ahead and play with it. As you can see, the font size stops growing when the viewport width is 840px and stops shrinking at 360px. Everything in between changes in linear fashion.

What if the user changes the root’s font size?

You may have noticed a little flaw with this whole approach: it only works as long as the root’s font size is the one you think it is — which is 16px in the previous example — and never changes.

We are converting the widths, 360px and 840px, to rem units by dividing them by 16 because that’s what we assume is the root’s font size. If the user has their preferences set to another root font size, say 18px instead of the default 16px, then that calculation is going to be wrong and the text won’t resize the way we’d expect.

There is only one approach we can use here, and it’s (1) making the necessary calculations in code on page load, (2) listening for changes to the root’s font size, and (3) re-calculating everything if any changes take place.

Here’s a useful JavaScript function to do the calculations:

// 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 )"

I’m deliberately leaving out how to inject the returned string into the CSS because there are a ton of ways to do that depending on your needs and whether your are using vanilla CSS, a CSS-in-JS library, or something else. Also, there is no native event for font size changes, so we would have to manually check for that. We could use setInterval to check every second, but that could come at a performance cost.

This is more of an edge case. Very few people change their browser’s font size and even fewer are going to change it precisely while visiting your site. But if you want your site to be as responsive as possible, then this is the way to go.

For those who don’t mind that edge case

You think you can live without it being perfect? Then I got something for you. I made a small tool to make make the calculations quick and simple.

All you have to do is plug the widths and font sizes into the tool, and the function is calculated for you. Copy and paste the result in your CSS. It’s not fancy and I’m sure a lot of it can be improved but, for the purpose of this article, it’s more than enough. Feel free to fork and modify to your heart’s content.

How to avoid reflowing text

Having such fine-grained control on the dimensions of typography allows us to do other cool stuff — like stopping text from reflowing at different viewport widths.

This is how text normally behaves.

It keeps the same width-to-font ratio. The reason we do this is because we need to ensure that the text has the right size at every width in order for it to be able to keep the same number of lines. It’ll still reflow at different widths but doing this is necessary for what we are going to do next. 

Now we have to get some help from the CSS character (ch) unit because having the font size just right is not enough. One ch unit is the equivalent to the width of the glyph “0” in an element’s font. We want to make the body of text as wide as the viewport, not by setting width: 100% but with width: Xch, where X is the amount of ch units (or 0s) necessary to fill the viewport horizontally.

To find X, we must divide the minimum viewport width, 320px, by the element’s ch size at whatever font size it is when the viewport is 320px wide. That’s 1rem in this case.

Don’t sweat it, here’s a snippet to calculate 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;

Now we can proceed to set the text’s 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`;
Umm, who invited you to the party, scrollbar?

Whoa, wait. Something bad happened. There’s a horizontal scrollbar screwing things up!

When we talk about 320px, we are talking about the width of the viewport, including the vertical scrollbar. So, the text’s width is being set to the width of the visible area, plus the width of the scrollbar which makes it overflow horizontally.

Then why not use a metric that doesn’t include the width of the vertical scrollbar? We can’t and it’s because of the CSS vw unit. Remember, we are using vw in clamp() to control font sizes. You see, vw includes the width of the vertical scrollbar which makes the font scale along the viewport width including the scrollbar. If we want to avoid any reflow, then the width must be proportional to whatever width the viewport is, including the scrollbar.

So what do we do? When we do this:

text.style.width = `${ 320 / calculateCh(text, "1rem") }ch`;

…we can scale the result down by multiplying it by a number smaller than 1. 0.9 does the trick. That means the text’s width is going to be 90% of the viewport width, which will more than account for the small amount of space taken up by the scrollbar. We can make it narrower by using an even smaller number, like 0.6.

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`;
So long, scrollbar!

You might be tempted to simply subtract a few pixels from 320 to ignore the scrollbar, like this:

text.style.width = `${ ( 320 - 30 ) / calculateCh( text, "1rem" ) }ch`;

The problem with this is that it brings back the reflow issue! That’s because subtracting from 320 breaks the viewport-to-font ratio.

The width of text must always be a percentage of the viewport width. Another thing to have in mind is that we need to make sure we’re loading the same font on every device using the site. This sounds obvious doesn’t it? Well, here’s a little detail that could throw your text off. Doing something like font-family: sans-serif won’t guarantee that the same font is used in every browser. sans-serif will set Arial on Chrome for Windows, but Roboto on Chrome for Android. Also, the geometry of some fonts may cause reflow even if you do everything right. Monospaced fonts tend to yield the best results. So always make sure your fonts are on point.

Check out this non-reflowing example in the following demo:

Non-reflowing text inside a container

All we have to do is now is apply the font size and width to the container instead of the text elements directly. The text inside it will just need to be set to width: 100%. This isn’t necessary in the cases of paragraphs and headings since they’re block-level elements anyway and will fill the width of the container automatically.

An advantage of applying this in a parent container is that its children will react and resize automatically without having to set their font sizes and widths one-by-one. Also, if we need to change the font size of a single element without affecting the others, all we’d have to do is change its font size to any em amount and it will be naturally relative to the container’s font size.

Non-reflowing text is finicky, but it’s a subtle effect that can bring a nice touch to a design!

Wrapping up

To cap things off, I put together a little demonstration of how all of this could look in a real life scenario.

In this final example, you can also change the root font size and the clamp() function will be recalculated automatically so the text can have the right size in any situation.

Even though the target of this article is to use clamp() with font sizes, this same technique could be used in any CSS property that receives a length unit. Now, I’m not saying you should use this everywhere. Many times, a good old font-size: 1rem is all you need. I’m just trying to show you how much control you can have when you need it.

Personally, I believe clamp() is one of the best things to arrive in CSS and I can’t wait to see what other usages people come up with as it becomes more and more widespread!

I didn’t realize clamp allows removing media queries to compute font-size with linear interpolation, thank you! I think your clampBuilder() can be replaced with pure CSS:

:root {
  --min-fs: 1;
  --max-fs: 1.5;
  --min-vw: 20;
  --max-vw: 45;
  --min-fs-rem: var(--min-fs) * 1rem;
  --max-fs-rem: var(--max-fs) * 1rem;
  --min-vw-rem: var(--min-vw) * 1rem;
  --slope: (var(--max-fs) - var(--min-fs)) * (100vw - var(--min-vw-rem)) / (var(--max-vw) - var(--min-vw));
  font-size: clamp(var(--min-fs-rem), var(--min-fs-rem) + var(--slope), var(--max-fs-rem));
        

I’ve been using this for scalable fonts:

font-size: calc(14px + (36 - 14) * ((100vw - 320px) / (2560 - 320));

What are the advantages of clamp over this – just less syntax and a possible avoidance of media queries? Which is the best supported? Thanks :)

Hi Kev,I’m not sure you’ll ever read this comment, but in the spirit of writing something interesting for anyone…

I’ve also been using your formula (using rem unit instead of px, though) for many years, thanks to Mike Riethmuller). I regret his method have not been mentioned in this post and compared with the “clamp method”. I also think that the only difference is a shorter syntax. calc() is supported by IE while clamp() is not.

The calc() trick is so cool! Only drawback is it won’t stop at the min and max limit, whereas clamp will. Drawback of clamp is it has less support at the moment.

So I’m actually using a combination of both from now on and I find it convenient. It would look like:

font-size:calc(16px + (28 - 16) * ((100vw - 360px) / 1560));
font-size: clamp(16px, calc(16px + (28 - 16) * ((100vw - 360px) / 1560)), 28px);

I’m working on a CMS where the user gets to pick min and max font-size, so I can capture this formulas in a ready-made template-like code and it saves lots of time without requiring javascript.

Safari only supports clamp() since 13.7. You can use the nested min()/max() trick as mentioned on MDN:
clamp(MIN, VAL, MAX) is resolved as max(MIN, min(VAL, MAX))

https://developer.mozilla.org/en-US/docs/Web/CSS/clamp

Please be careful with maximum text size, particularly on sites/pages that face the general public or employees. If you prevent the text from scaling up 200%, then that is a WCAG SC 1.4.4 failure at Level AA. Viewport units have their own call-out as a major risk in WCAG.

No matter what technique you use, be sure that the page text can be zoomed at least 200%. Unsurprisingly, I have written about responsive type and zoom, and have cautioned against min(), max(), and clamp().

I understand the examples are for demonstration purpose only, but none allow me to scale the text past 175%. If the example code was copied as-is into a project, that project would likely fail an accessibility audit.

Hey Adrian, thanks for sharing that! I was not aware. However, I’m not following that you cannot scale past 175%. Probably because I’m testing incorrectly. How can I check this? I switched on “Only Zoom Text” in Firefox and set the Zoom to 200% and that works fine. I can understand for “vh” this is an issue, but how is this blocking for “vw”, like in the example above? Thanks

Bregt, open the debug mode of your pens such as cdpn.io/pprg1996/debug/yLONLPv, and then use the native browser scaling feature — on Windows it is Ctrl + mouse scroll wheel up or Ctrl + + (yup, that is the plus key).

As you zoom, pay attention to the zoom level in the address bar. You should notice the text stops getting larger at some point.

To make this more obvious, try setting your viewport width to 800px. Now try zooming. Observe how the text gets smaller as you try to zoom.

If you want, I can make you a video or screen shots.

Bregt, I made a video to show the issue along with some screen shots:

https://adrianroselli.com/2019/12/responsive-type-and-zoom.html#Update04

Now try it with 800px viewport. You will see that zooming the page makes the text shrink.

Thanks Adrian. I noticed it too, but did not have this issue in wide screens. What could be a good approach trying to use clamp() and max()/min()?

I use a larger rem compared to the vw addition in my function (eg. max(2.1875rem, min(1.9375rem + 1.1111111111vw, 2.8125rem), I use max(min()) instead of clamp() to support older Safari browsers).

So could this technique be used on other attributes besides font size?

Could line-height, padding, margin, border widths, etc. also be flexible?

I’ve been using Indrix Pass’s mixin for a while with great success but am all for a more native approach: http://sassmeister.com/gist/7f22e44ace49b5124eec

The technique seems cool and all, but can I just ask a simple question. (or few)

Why do we want to scale the text bigger on bigger screen? I mean, why go through all this trouble to begin with? I mean, are we assuming something about how close or far the user is to the screen? Is there some unforseen benefit to the user that I can not see here by taking away control from the regular UI and text zoom options that the user already has?

Mmm… Tested it in Chrome. Changing default font size in browser settings has on effect on the nodes, which font size is defined by the clamp function. I’m not talking about root font size. I’m talking about the situation when root font size is not explicitly specified and user can change it in settings on his own.

Here is a Sass function of this. Input sizes are in pixels (because I find it more intuitive).

@function betterClamp($minSize, $maxSize, $minWidth: 480, $maxWidth: 1536) {
// convert to rem
$minSize: $minSize / 16;
$maxSize: $maxSize / 16;
$maxWidth : $maxWidth / 16;
$minWidth : $minWidth / 16;
// do calculations
$slope: ($maxSize - $minSize) / ($maxWidth - $minWidth);
$yAxisIntersection: -$minWidth * $slope + $minSize;
$preferredValue: #{$yAxisIntersection * 1rem} + #{$slope * 100vw};
// output as rem
$minSize: $minSize * 1rem;
$maxSize: $maxSize * 1rem;
@return clamp($minSize, $preferredValue, $maxSize);