Experimental Solution For "Container Query" Font Sizing
Tags: css, javascript, webdev
Published: 2021-02-04 04:22:52

> The following is an experimental technique. Please carefully consider the _cons_ listed at the end before using it in your project.

**Good news!** Container queries have been [proposed to the CSSWG by Miriam Suzanne](https://gist.github.com/mirisuzanne/748169312f110d6246e092945673b16e), and are being [prototyped in Chromium](https://bugs.chromium.org/p/chromium/issues/detail?id=1145970).

I've previously written [about CSS layout that comes close to container queries](https://moderncss.dev/container-query-solutions-with-css-grid-and-flexbox/). I've also written about [fluid typography with viewport units](https://moderncss.dev/generating-font-size-css-rules-and-creating-a-fluid-type-scale/).

But this article is about fluid typography relative to _the parent container_.

## The Problem

Existing CSS-only solutions (like my article linked previously) rely on _viewport units_ in order to trigger a change that will scale typography.

Why? Because there isn't a CSS-only way to get the width of the parent, and any media query solutions rely on the viewport as well.

Here's a comparison of these two options:

```css
/* Fluid typography */
h2 {
  /* clamp: min | preferred | max */
  font-size: clamp(1.5rem, 4vw, 2rem);
}

/* Media query */
h2 {
  font-size: 1.5rem;
}

@media (min-width: 60rem) {
  h2 {
    font-size: 2rem;
  }
}
```

What we really want is to have a smaller font-size when a container is narrow, which may or may not mean the _viewport_ is narrow.

## Solution

Now, we _could_ devise an entirely JavaScript solution, and that might be your first impulse!

However, using vanilla CSS, we can get most of the way there thanks to `clamp` and `calc`. The missing ingredient towards a solution is obtaining the container width, and that's where we need a little help from JavaScript.

But let's start at the top and work out what our calculation even needs to look like.

We're going to consider this solution within the context of a card component, and specifically the card headline which is set as an `h3`.

First, we need to come up with minimum and maximum `font-size` values for our `h3`, which we'll create as CSS custom properties:

```css
.card h3 {
  --max: 2rem; /* 32px */
  --min: 1.25rem; /* 20px */
}
```

But we also need to have a target "midsize" value, and in this case we'll select `28px`.

Let's set our `font-size` to this midsize value as a starting point:

```css
.card h3 {
  font-size: 28px;
}
```

This midsize value is the first half of how we'll work out the calculation needed to fluidly resize the typography relative to the container.

Ultimately, the calculation we need is to take a value that represents the width of a single character and determine what that size is relative to the total container width. So now we need to work out a starting container size to arrive at a value to represent that character width.

In our cards context, let's assume that usually, we have three cards in a row when space allows, which produces fairly narrow cards. But maybe we don't always have enough content for three cards, so if there are two cards then they would each share half of the width of the row. And we're going to say that the 2-up cards represent a "midsize" card width.

Given an additional layout constraint which is the max-width we're allowing for the row of cards - `120ch` - in this exact example that gives us a computed midsize card _outer_ width of `576px`.

We also have `1rem` of padding applied around the text content of the card, and we want to subtract that to get the width relative to the content area only. So this gives us a width of `544px`.

Now, we're going to allow a bit of wiggle room in the numbers here and say that the font-size value is equal to the width of one character as well (which is typically not really true for non-monospace font families). But! It will make our calculation easier, and close enough :)

So, to get our value that represents a single character width, we divide our midsize font-size - `28` - by our midsize container width - `544`:

```js
// Rounded down for a nice whole number
28/544 = 0.05; 
```

Which allows us to prepare our formula:

```js
0.05 * [card-width]
```

But... how do we get both the formula and the dynamic `card-width` value into our CSS?

The answer to this is a combination of `clamp`, `calc` and CSS custom properties.

Let's go ahead and update our rule and then discuss:

```css
.card h3 {
  --max: 2rem; // 32px
  --min: 1.25rem; // 20px

  font-size: 
    clamp(
      var(--min), 
      calc(0.05px * var(--card-width)), 
      var(--max)
    );
}
```

We dropped `clamp()` into our initial examples, but if you haven't used it, the way to read it is that it accepts a minimum, a preferred, and a max value. It's a very cool CSS function with many applications beyond fluid font-sizing.

So, we dropped in the `min` and `max` CSS custom properties that we already had set up. And we've added a new property of `card-width`, and for now we're defaulting it to the midsize card value that we found.

But in the middle, we include `calc()` which is a CSS function that allows us to perform math calculations. And, it's fortunately friendly to dynamic values! Eventually, it will act as guardrails to prevent the computed middle value from shrinking below the minimum, or growing above the maximum.

We've provided `calc()` our value of `0.05`, and then multiply it by the `card-width` custom property. And while the addition of the `px` to this value probably looks funny, it's because the `card-width` is going to be unitless, and we need the final computed value to have a unit to work.

But wait! Where are we setting the `card-width` custom property?

For our cards, we're using CSS grid and a particular method that will automagically resize the grid columns containing the cards. Using this, all cards will resize at the same rate and always be equal width as each other.

So for this example, we're going to set the `card-width` custom property at the level of the `.card-wrapper` class which is the containing element for the cards. This way, we can update it once, and the value will cascade to the cards. Otherwise, we would have to individually update the value _per card h3_ if we scoped it more specifically.

Here's the extra rule:

```css
.card-wrapper {
  --card-width: 544;
}
```

For now, we're defaulting this to the size we found to be our midsize card width. You'll also notice it's unitless, which is for simplicity when we update it with JavaScript in a moment.

The result of our CSS so far is still static and will be `28(ish)px`.

It's time to add in the JavaScript to help us compute the actual card width.

We'll create a tiny function that we'll eventually call on load and also upon window resize.

To start off, we'll create some constants. Since in this exact context all cards will be equal width, we only need to examine the first one we find.

```js
const updateCardFontSize = () => {
  const card = document.querySelector(".card");
  const cardWrapper = document.querySelector(".card-wrapper");
  const cardWidth = card.clientWidth;
};
```

Finally, we add one last line that will use the `cardWidth` computed value, which is assumed to be pixels, as the value of the `--card-width` CSS custom property. This is possible by using `style.setProperty()` which intakes the full custom property name, and then the desired value:

```js
const updateCardFontSize = () => {
  // ...constants

  cardWrapper.style.setProperty("--card-width", cardWidth);
};
```

Now all that's left is to initiate this function on load and on window resize (the full demo includes a `debounce` function as well).

```js
updateCardFontSize();
window.addEventListener("resize", updateCardFontSize);
```

## Demo

This CodePen demonstrates all of these pieces together, and you are encouraged to [open the full pen](https://codepen.io/5t3ph/full/OJbVKeB) to resize and get the full effect! You can also add and remove cards to see how that alters the allowed `font-size`. For example, the computed size won't reach the `max` value unless there's only one card and the viewport is larger than ~`670px`.

{% codepen https://codepen.io/5t3ph/pen/OJbVKeB %}

_[Open the full pen](https://codepen.io/5t3ph/full/OJbVKeB)_

## Pros and Cons

**Reminder: this technique is experimental**! 

I hope you've learned some useful things about `clamp`, `calc`, and CSS custom properties, but please consider the following before using this technique in your projects.

If you do decide to use it, consider limiting it to a single section or component, and not _all_ typography across your application.

Evaluate if you really need this technique, or if viewport sizing via `clamp` or a series of media queries is best suited for your goals.

### Pros

- `font-size` is relative to the container, rather than the viewport.
- Possibly reduce reliance on text truncation methods due to the technique keeping a similar number of characters per line across container sizes

### Cons

- Potential of a brief flash at the undesired `font-size`, both annoying to users and possibly impacting Cumulative Layout Shift (CLS) performance scores
- Difficult to scale across multiple contexts and typography elements
- Potential to hit performance issues if trying to attach to too many elements
- `font-size` "ramps" need to be carefully constructed to retain visual hierarchy throughout resizing
- Possibility of not allowing zooming to increase `font-size` as required by WCAG accessibility guidelines (also a pitfall of fluid typography based on viewport units)

> What do you think? Have a suggestion for how this could be improved? Spot an error in my math? Drop a comment! If you enjoy CSS-focused front-end tips and tutorials, [follow @5t3ph on Twitter](https://twitter.com/5t3ph)!