Linearly Scale font-size with CSS clamp() Based on the Viewport

Responsive typography has been tried in the past with a slew of methods such as media queries and CSS calc().

Here, we’re going to explore a different way to linearly scale text between a set of minimum and maximum sizes as the viewport’s width increases, with the intent of making its behavior at different screen sizes more predictable — All in a single line of CSS, thanks to clamp().

The CSS function clamp() is a heavy hitter. It’s useful for a variety of things, but it’s especially nice for typography. Here’s how it works. It takes three values: 

clamp(minimum, preferred, maximum);

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.

Line chart with the vertical axis measured in font size rem unites from 0 to 4, and the horizontal axis measuring viewport width from 0 to 1,060 pixels. There are four blue points on the grid with a blue line connecting them.

What we are trying to achieve with clamp() is called linear interpolation: getting intermediate information between two data points.

Here are the four steps to do this:

Step 1

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 has a number of lines at a certain viewport width…
…and wraps it’s lines to fit another width

But now, with the control we have, we can make text keep the same number of lines, breaking on the same word always, on whatever viewport width we throw at it.

Viewport width = 400px
Viewport width = 740px

So how do we do this? To start, the ratio between font sizes and viewport widths must stay the same. In this example, we go from 1rem at 320px to 3rem at 960px.

320 / 1 = 320
960 / 3 = 320

If we’re using the clampBuilder() function we made earlier, that becomes:

const text = document.querySelector( "p" );
text.style.fontSize = clampBuilder( 320, 960, 1, 3 );

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.

Viewport width = 650px
Viewport width = 670px

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!


The post Linearly Scale font-size with CSS clamp() Based on the Viewport appeared first on CSS-Tricks.

You can support CSS-Tricks by being an MVP Supporter.

Dealing With Stale Props and States in React’s Functional Components

There’s one aspect of JavaScript that always has me pulling my hair: closures. I work with React a lot, and the overlap there is that they can sometimes be the cause of stale props and state. We’ll get into exactly what that means, but the trouble is that the data we use to build our UI can be totally wrong in unexpected ways, which is, you know, bad.

Stale props and states

Long story short: it’s when code that is executed asynchronously has a reference to a prop or state that is no longer fresh, and thus, the value it returns is not the latest one.

To be even more clear, let’s play around with the same stale reference example React has in its documentation.

function Counter() {
  const [count, setCount] = useState(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + count);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}

(Live demo)

Nothing fancy here. We have a functional component named Counter. It keeps track of how many times the user has clicked one button and shows an alert that displays how many times that button was clicked when clicking another button. Try this:

  1. Click the “Click me” button. You are going to see the click counter go up.
  2. Now click the “Show alert”button. Three seconds should go by and then trigger an alert telling you how many times you clicked the “Click me” button.
  3. Now, click the “Show alert” button again and quickly click the “Click me” button before it triggers the alert in three seconds.

See what happens? The count shown on the page and the count shown in the alert do not match. The number in the alert is not just some random number, though. That number is the value the count variable had in the moment the asynchronous function inside the setTimeout was defined, which is the moment the “Show alert” button is clicked.

That’s just how closures work. We’re not going to get into the specifics of them in this post, but here are some docs that cover them in greater detail.

Let’s focus on how we can avoid these stale references with our states and props.

React offers a tip on how to deal with stale dates and props in the same documentation where the example was pulled.

If you intentionally want to read the latest state from some asynchronous callback, you could keep it in a ref, mutate it, and read from it.

By keeping the value  asynchronously in a ref, we can bypass stale references. If you need to know more about ref in functional components, React’s documentation has a lot more information.

So, that begs the question: How can we keep our props or state in a ref?

Let’s do it the dirty way first.

The dirty way to store props and state in a ref

We can easily create a ref using useRef() and use count as its initial value. Then, wherever the state is being updated, we set the ref.current property to the new value. Lastly, use ref.current instead of count in the asynchronous part of our code.

function Counter() {
  const [count, setCount] = useState(0);
  const ref = useRef(count); // Make a ref and give it the count

  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + ref.current); // Use ref instead of count
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button
        onClick={() => {
          setCount(count + 1);
          ref.current = count + 1; // Update ref whenever the count changes
        }}
      >
        Click me
      </button>
      <button
        onClick={() => {
          handleAlertClick();
        }}
      >
        Show alert
      </button>
    </div>
  );
}

(Live demo)

Go ahead and do the same as last time. Click “Show alert” and then click “Click me” before the alert is triggered in three seconds.

Now we have the latest value!

Here’s why it works. When the asynchronous callback function is defined inside setTimeout, it saves a reference to the variables it uses, which is count in this case. This way, when the state updates, React not only changes the value but the variable reference in memory is completely different as well.

This means that — even if the state’s value is non-primitive — the variable you are working with in your asynchronous callback is not the same in memory. An object that would typically keep its reference throughout different functions now has a different value.

How does using a ref solve this? If we take a quick look at React’s docs again, we find an interesting, but easy-to-miss, bit of information:

[…] useRef will give you the same ref object on every render.

It doesn’t matter what we do. Throughout the lifetime of your component, React will give us the exact same ref object in memory. Any callback, no matter when it’s defined or executed, is working with the same object. No more stale reference.

The cleaner way to store props and state in a ref

Let’s be honest… using a ref like that is an ugly fix. What happens if your state is being updated in a thousand different places? Now you have to change your code and manually update the ref in all those places. That’s a no-no.

We are going to make this more scalable by giving ref the value of the state automatically when the state changes.

Let’s start by getting rid of the manual change to the ref in the “Click me”button.

Next, we make a function called updateState that is called whenever we need to change the state. This function takes the new state as an argument and it sets the ref.current property to the new state and updates the state as well with that same value.

Finally, let’s substitute the original setCount function React gives us with the new updateState function where the state is being updated.

function Counter() {
  const [count, setCount] = useState(0);
  const ref = useRef(count);

  // Keeps the state and ref equal
  function updateState(newState) {
    ref.current = newState;
    setCount(newState);
  }

  function handleAlertClick() { ... }

  return (
    <div>
      <p>You clicked {count} times</p>
      <button
        onClick={() => {
          // Use the created function instead of the manual update
          updateState(count + 1);
        }}
      >
        Click me
      </button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}

(Live demo)

Using a custom hook

The cleaner solution works just fine. It gets the job done just like the dirty solution, but only calls a single function to update the state and ref.

But guess what? We can do better. What if we need to add more states? What if we want to do this in other components too? Let’s take the state, ref and updateState function and make them truly portable. Custom hooks to the rescue!

Outside the Counter component, we are going to define a new function. Let’s name it useAsyncReference. (It can be named anything, really, but note that it’s common practice to name custom hooks with “use” as a prefix.) Our new hook will have a single parameter for now. We’ll call it value.

Our previous solution had the same information stored twice: once in the state and once in the ref. We are going to optimize that by keeping the value just in ref this time. In other words, we will create a ref and give it the value parameter as its initial value.

Right after the ref, we will make an updateState function that takes the new state and sets it to the ref.current property.

Lastly, we return an array with ref and the updateState function, very similar to what React does with useState.

function useAsyncReference(value) {
  const ref = useRef(value);

  function updateState(newState) {
    ref.current = newState;
  }

  return [ref, updateState];
}

function Counter() { ... }

We are forgetting something! If we check the useRef documentation, we learn that updating a ref does not trigger a re-render. So, while ref has the updated value, we wouldn’t see the changes on screen. We need to force a re-render every time ref gets updated.

What we need is a fake state. The value doesn’t matter. It’s only going to be there to provoke the re-render. We can even ignore the state and only keep its update function. We are calling that update function forceRender and giving it an initial value of false.

Now, inside updateState, we force the re-render by calling forceRender and passing it a state different to the current one after setting ref.current to newState.

function useAsyncReference(value) {
  const ref = useRef(value);
  const [, forceRender] = useState(false);

  function updateState(newState) {
    ref.current = newState;
    forceRender(s => !s);
  }

  return [ref, updateState];
}

function Counter() { ... }

Take whatever value it has and return the opposite. The state doesn’t really matter. We are merely changing it so React detects a change in state and re-renders the component.

Next, we can clean the Count component and remove the previously used useState, ref and updateState function, then implement the new hook. The first value of the returned array is the state in the form of a ref. We’ll keep calling it count, where the second value is the function to update the state/ref. We’ll continue calling it setCount.

We also have to change the references to the count since now that they all must be count.current. And we must call setCount instead of calling updateState.

function useAsyncReference(value) { ... }

function Counter() {
  const [count, setCount] = useAsyncReference(0);

  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + count.current);
    }, 3000);
  }

  return (
    <div>
      <p>You clicked {count.current} times</p>
      <button
        onClick={() => {
          setCount(count.current + 1);
        }}
      >
        Click me
      </button>
      <button onClick={handleAlertClick}>Show alert</button>
    </div>
  );
}

Making this work with props

We have a truly portable solution for our problem. But guess what… there’s still a little more to do. Specifically, we need to make the solution compatible with props.

Let’s take the “Show alert” button and handleAlertClick function to a new component outside the Counter component. We are gonna call it Alert and it’s going to take a single prop called count. This new component is going to show the count prop value we are passing it in an alert after a three second delay.

function useAsyncReference(value) { ... }

function Alert({ count }) {
  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + count);
    }, 3000);
  }

  return <button onClick={handleAlertClick}>Show alert</button>;
}

function Counter() { ... }

In Counter, we’re swapping the “Show alert” button for the Alert component. We’ll pass count.current to the count prop.

function useAsyncReference(value) { ... }

function Alert({ count }) { ... }

function Counter() {
  const [count, setCount] = useAsyncReference(0);

  return (
    <div>
      <p>You clicked {count.current} times</p>
      <button
        onClick={() => {
          setCount(count.current + 1);
        }}
      >
        Click me
      </button>
      <Alert count={count.current} />
    </div>
  );
}

(Live demo)

Alright, time to run through the testing steps again. See? Even though we are using a safe reference to the count in Counter, the reference to the count prop in the Alert component is not asynchronously safe and our custom hook is not suitable to use with props… yet.

Lucky for us, the solution is fairly simple.

All we have to do is add a second parameter to our useAsyncReference hook named isProp, with false as the initial value. Just before we return the array with ref and updateState, we set up a condition. If isProp is true, we set the ref.current property to value and only return ref.

function useAsyncReference(value, isProp = false) {
  const ref = useRef(value);
  const [, forceRender] = useState(false);

  function updateState(newState) {
    ref.current = newState;
    forceRender(s => !s);
  }

  if (isProp) {
    ref.current = value;
    return ref;
  }

  return [ref, updateState];
}

function Alert({ count }) { ... }

function Counter() { ... }

Now let’s update Alert so that is uses the hook. Remember to pass true as a second argument to useAsyncReference since we are passing a prop and not a state.

function useAsyncReference(value) { ... }

function Alert({ count }) {
  const asyncCount = useAsyncReference(count, true);

  function handleAlertClick() {
    setTimeout(() => {
      alert("You clicked on: " + asyncCount.current);
    }, 3000);
  }

  return <button onClick={handleAlertClick}>Show alert</button>;
}

function Counter() { ... }

(Live demo)

Give it another try. Now it works perfectly whether you use states or props.

One last thing…

There’s one last change I’d like to make. React’s useState docs tell us that React will bail out of a re-render if the new state is identical to the previous one. Our solution doesn’t do that. If we pass the current state again to the hook’s updateState function, we will force a re-render no matter what. Let’s change that.

Let’s put the body of updateState inside an if statement and execute it when ref.current is different than the new state. The comparison must be done with Object.is(), just like React does.

function useAsyncReference(value, isProp = false) {
  const ref = useRef(value);
  const [, forceRender] = useState(false);

  function updateState(newState) {
    if (!Object.is(ref.current, newState)) {
      ref.current = newState;
      forceRender(s => !s);
    }
  }

  if (isProp) {
    ref.current = value;
    return ref;
  }

  return [ref, updateState];
}

function Alert({ count }) { ... }

function Counter() { ... }

Now we are finally done!


React can sometimes seem like a black box that is full of little quirks. Those quirks might be daunting to deal with, like the one we just tackled. But if you are patient and enjoy being challenged, you’ll soon realize it’s an awesome framework and a pleasure to work with.

The post Dealing With Stale Props and States in React’s Functional Components appeared first on CSS-Tricks.