---
title: Fluid typography in 2024
date: 2024-03-15
tags: [design, css]
description: The line width you choose is only a maximum value. If the screen is to small, lines can end up significantly shorter than you expected. Let's go down this rabbit hole!
---

![Screenshot of the techniques from this article in action](preview.png)

The basics of typography are quickly explained: Body text should have 60-80
characters per line, and line-height should be bigger if there are more
characters on a line.

Usually in web design, font and font size is defined by user preferences (and
we should respect that!). We as designers are left with picking a line width
and line height, for example like this:

```css
body {
  max-inline-size: 40em;
  line-height: 1.6;
}
h1, h2 {
  line-height: 1.2;
}
```

However, the line width you choose is only a maximum value. If the screen is to
small, lines can end up significantly shorter than you expected, and that can
throw off your composition.

A simple approach would be to choose values that work reasonably well on both
small and large screens. But what fun would that be? No, let's go down this
rabbit hole!

## Fluid font size

The first thing we could try is to shrink the font size so that the number of
characters per line does not decrease, or at least not as much. For example, we
could add something like this:

```css
body {
  font-size: clamp(0.8em, 0.5em + 1vi, 1.2em);
}
```

*Note: If you use `em` in a font-size definition, it refers to the font-size of
the parent element.*

This approach does of course have its limits, we cannot shrink the font
indefinitely. But it can provide some breathing room.

However, we immediately are faced with a challenge: With the example above,
there are no guarantees that the font actually needs resizing. Maybe the users
already configured their font settings to match the screen, so we make it worse
for them instead of better.

So here is what I propose: Imagine we had a custom property `--factor` that
told us how much space we have, going from `0` for no space at all to `1` for
when we hit `max-inline-size`. That would give us a simple way to only scale
the font size if needed:

```css
body {
  font-size: calc(0.8em + 0.4em * var(--factor));
}
```

## Fluid line height

Long lines are hard to read because it is easy to jump to the previous line by
accident, reading the same text again and again. Increasing the line height can
help in those cases. [Tim Brown][molten-leading] already wrote about ways to
automatically adjust line-height in 2012. We can again use `--factor` for a
simple solution:

```css
body {
  line-height: calc(1 + 0.6 * var(--factor));
}
```

However, the line height should not so much depend on the line width in pixels,
but on the number of characters per line. That is why in the beginning I used a
smaller line-height for headings. So we have to throw the font-size into the
mix:

```css
body {
  line-height: calc(1 + 0.6 * var(--factor) / 1em);
}
```

Unfortunately, it's not that simple though. For one, it is currently not
possible to divide by values with units (more on that later). But even if that
were possible, that wouldn't help because the `em` would be evaluated on the
body element, not on the element where it is applied. So instead, we will add
yet another custom property:

```css
:root {
  --font-size: 1;
}
:root * {
  font-size: calc(var(--font-size) * 1rem);
  line-height: calc(1 + 0.6 * var(--factor) / var(--font-size));
}
h1 {
  --font-size: 2;
}
```

## Fluid modular scale

The relations between line length, font size, and line height we discussed so
far were all linear. However, some designers like to work with [modular
scales][modular-scale], i.e. power relations. For example:

```css
:root {
  --scale: 1.5;
}
h2 {
  --font-size: var(--scale);
}
h1 {
  --font-size: calc(var(--scale) * var(--scale));

  /* see https://caniuse.com/mdn-css_types_pow */
  --font-size: pow(var(--scale), 2);
}
```

Comig back to fluid typography, the people at [utopia][utopia] propose to also
adapt that base scale (they say this is what they do, but actually they do
linear interpolation between the powers). Again, this can easily be done using
`--factor`:

```css
:root {
  --scale: calc(1 + 0.5 * var(--factor));
}
```

## The catch: calculating `--factor`

So far we looked at relations between different typographic measurements. The
exact formulas and constants can be modified to match your personal taste.
However, one puzzle piece is still missing: the `--factor` property. And
unfortunately, that is not easy to come by. Spoilers: I don't have a perfect
solution for this yet.

Ideally, we want something like this:

```css
:root {
  --factor: min(100vi / 40em, 1);
}
```

As I mentioned before, [CSS Values and Units Module Level 3][css-values-3], the
current version of the relevant standard, does not allow to multiply or divide
by values with units. [Level 4][css-values-4] will change that, but it is not
yet implemented anywhere ([Firefox ticket][bugzilla]).

In the meantime, we can use JavaScript:

```js
var setFactor = function() {
  var style = getComputedStyle(document.body);
  var factor = parseFloat(style.inlineSize) / parseFloat(style.maxInlineSize);
  document.documentElement.style.setProperty('--factor', factor);
};

window.addEventListener('resize', setFactor);
window.addEventListener('load', setFactor);
```

But there is another issue: The maximum line width is defined as `40em`, which
depends on the current font size. The font size in turn depends on `--factor`,
which depends on the maximum line width. So there is a circular dependency.

In practice, this circle can be broken because there is an absolute maximum
font size. In our case, that is `1.2 * 40 * 1rem`, were `rem` refers to the
user defined font size. In case you had wondered: That is why I never set
`font-size` on `:root`. If I had done that, it would be impossible to get to
the original, user-defined value. Unfortunately, this also means that we can no
longer use the `rem` unit for styling.

The rest is left as an exercise to the reader. If you are interested, I have
prepared a [complete demo](./demo/).

## Conclusion

The ideas and the reasoning I laid out in this article are hopefully a useful
overview. There are many more ways to approach this topic, but these ideas are
what felt most clear and succinct to me. Still, the specific techniques are not
really suitable for production just yet. Most importantly, the requirement on
JavaScript is a clear no-go for me.

But there is hope on the horizon. Before getting into this topic, I wasn't
really aware of [CSS Values and Units Level 4][css-values-4]. It already gave
us widespread support for `min()`, `max()`, and `clamp()`, which is awesome.
Division with units could be the final puzzle piece we need to get real fluid
typography.

Once the specification has settled and we get real-world implementations, we
can start refining these ideas and come up with robust, reusable solutions. But
before that can happen, browsers need to work out all the complex circular
dependencies. And I am not yet sure this will happen anytime soon.

## Bonus: some more ideas

-	It could also make sense to adapt line-height to the x-height of the font.
	This could be achieved by combining the `em`, `ch`, and `ex` units. But it
	would again require division by values with units.

-	[Container queries][container-queries] and the `cqi` unit could be useful to
	apply these techniques to individual components.

-	Vertical rhythm is a very different approach to typography than what I
	described here: Instead of adjusting the line height to the font size, you
	start with a fixed line grid and fit the elements in there. That is
	especially useful for newspaper-style layouts where you want the lines in
	neighboring columns to match. I rarely see this on the web, but it could be
	interesting to bring these ideas together.

- I tried to use [static interpolation][static-interpolation] (using paused
	animations to interpolate between values) for this, but found `calc()` easier
	to work with. Still, this is an interesting technique that could provide some
	benefits. [Scott Kellum][intrinsic-styles] has used this approach with some
	success.

[molten-leading]: https://tbrown.org/notes/2012/02/03/molten-leading-or-fluid-line-height/
[modular-scale]: https://www.modularscale.com/
[utopia]: https://utopia.fyi/blog/css-modular-scales/
[css-values-3]: https://www.w3.org/TR/css-values-3/#calc-type-checking
[css-values-4]: https://www.w3.org/TR/css-values-4/#calc-type-checking
[bugzilla]: https://bugzilla.mozilla.org/show_bug.cgi?id=1827404
[container-queries]: https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_containment/Container_queries
[static-interpolation]: https://youtu.be/I_fBM1cEc_8?t=1383
[intrinsic-styles]: https://css-tricks.com/intrinsic-typography-is-the-future-of-styling-text-on-the-web/
