Fluid typography in 2024
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:
body {max-inline-size: 40em;
line-height: 1.6;
}, h2 {
h1line-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:
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:
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 already wrote about ways to automatically adjust line-height in 2012. We can again use --factor
for a simple solution:
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:
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:
: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, i.e. power relations. For example:
: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 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
:
: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:
:root {
--factor: min(100vi / 40em, 1);
}
As I mentioned before, CSS Values and Units Module Level 3, the current version of the relevant standard, does not allow to multiply or divide by values with units. Level 4 will change that, but it is not yet implemented anywhere (Firefox ticket).
In the meantime, we can use JavaScript:
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.
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. 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
, andex
units. But it would again require division by values with units.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 then 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 (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 has used this approach with some success.