CSS Fluid Typography - Clamp() vs Calc()

When working on a project there is hardly ever enough time to thoroughly research every little detail. I'm currently building a Statamic starter kit for my agency however, and finally taking the time to come up with a near-optimal solution for most problems.

Fluid Typography and the Problem it Solves

When designing a website we usually pick font-sizes for mobile and desktop screens, maybe tablet as well if the design is very complex. So we have two or three well defined screen sizes where everything looks great, utilizing CSS media queries to change the font sizes as the screen gets bigger.

In reality there are a lot more resolutions options of course, everything from compact smart phones to ultra-wide screens. Using the above method usually leads to an output that looks great on some screens, but a little off to totally awful on others.

One possible solution is to keep adding more media break points and intermediate font sizes until the result is acceptable on all screens. Or we can define a font size that automatically grows with the screen size - a technique called fluid typography.

This much more elegant solution uses the CSS unit viewport width (vw), referring to the width of the user's screen, to calculate the font-size. There are two widely used ways of achieving this, and since I'm looking for a permanent solution I decided to compare them and pick the best one for our future projects.

Calc()-ulating our font-size

A solution that has been around for a while uses the CSS calc() function to calculate (hence the name) the font-size. First we define a minimum and a maximum font size in pixels. Then we pick two break points:

  • A minimum screen width, where we want to start resizing the font

  • A maximum screen width where we want to stop the resizing and stick with the maximum font size

These four variables can then be put into a simple formula: min-font-size + (max-font-size - min-font-size) * (100vw - min-screen-width) / (max-screen-width - min-screen-width)

To make this a little easier let's look at an example and say that we want the h1 font to be 20px tall on small screens and 28px on large screens. We want to start adjusting the size at a screen width of 640px and end at 1280px.

That leaves only 100vw as a variable that can still change the result. Of course this is the current screen width (which is automatically converted into a pixel value). Let's assume your browser window is 800px wide, then the font-size would be 20px + (28px - 20px) * (800px - 640px) / (1280px - 640px), which comes out as exactly 22px.

But we obviously don't want to calculate this value by hand every time, so here's how this would look implemented in CSS using the TailwindCSS default breakpoints:

/ressources/css/site.css (excerpt)
h1 { font-size: 20px; } @screen sm { h1 { font-size: calc(20px + (28 - 20) * (100vw - 640px) / (1280 - 640)); } } @screen xl { h1 { font-size: 28px; } }
Implementing fluid typography with clamp() and TailwindCSS media queries

The formula may look a bit confusing at first, but it's essentially just ensuring a linear growth of the font-size between the minimum and maximum at the respective breakpoints. A simple and elegant solution, much better than adding a handful of breakpoints for odd screen sizes.

Using Clamp() to set the Boundaries

There is another approach however, which uses the fairly new clamp() method (on top of calc() as used above) and doesn't need any media queries at all. To achieve this we need to supply the function with three values: a minimum font-size, a preferred font-size, and a maximum font-size: font-size: clamp(min-font-size, preferred-font-size, max-font-size)

This way the browser will use formula from before to calculate the font-size no matter what the screen size is. Since that would result in values too little (below the min-screen-width) or too large (above the max-screen-width) we use clamp().

This function ensures we only use values within our desired interval defined by min-font-size and max-font-size. If the calculated value becomes too small (or big) we automatically default to the minimum (or maximum) value defined in clamp().

With this we can reduce our CSS from the first solution to a single statement, this time using rem instead of px as our font-size unit. Since we are using the TailwindCSS default root font-size of 16px our starting size of 20px equals 1.25rem - all other values have also been converted accordingly.

/ressources/css/site.css (excerpt)
h1 { fontSize: clamp(1.25rem, calc(1.375rem + (1.75 - 1.25) * ((100vw - 40rem) / (64 - 40))), 1.75rem); }
Setting the fluid font-size using clamp()

The font-size calculated by this solution will be exactly the same as that of the first option. The only two differences are using clamp() instead of media queries and using rem as unit instead of px.

Which is better?

So which one should you go with? How do these two seemingly minor differences affect your project? Here are the two ways it affects your website and my way of picking the best solution for our starter kit.

Browser Support

When you've developed websites for nearly 20 years, both calc() and clamp() look like modern magic at first. I'm used to having to support all sorts of outdated browsers and therefor often shy away from using cutting edge features.

The caniuse-statistics for calc() (as of January 2022) show well over 96% support, even Internet Explorer as far back as version 9 has at least partial support. In contrast, clamp() is only supported on 91.4% of all browsers, mainly because it's a rather recent addition that wasn't even supported in Firefox until May of 2020.

5% of browsers is not a small number of potential users. And even though in theory we dropped support for IE quite a while ago, there might still be clients in old-fashioned industries that would probably make us create an IE-friendly solution instead.

Accessibility

While you might not notice any difference in output when using rem instead of px, it does affect the accessibility of your website. Visually impaired users - that includes a lot of people with less than perfect eye-sight - often set their browser to a larger default font-size, making it easier for them to read things online.

By doing so they overwrite the font-size defined in the :root element which is used when calculating font-sizes using rem. So when a user sets the root font-size to 20px (from TailwindCSS' default of 16px):

  • a font defined as 20px remains at 20px

  • a font defined as 1.25rem (formerly 20px) is now 25px tall

Obviously the second option is the one desired by the user to improve legibility.

Additionally, TailwindCSS already uses rem units for paddings, margins, and sizing. So if your container is sized rem but the text inside in px, your layout would probably shift if the user changes the root font-size.

My Conclusion

So to sum up the difference, the first solution with calc() and media-queries has better browser support while the clamp() approach is better for visually-impaired users.

Weighing these two differences, the decision was easy for me. Not only is accessibility becoming more and more important - last but not least for legal reasons - but the support for clamp() should increase every day, meaning this downside will be less important soon.

Using Fluid Typography in TailwindCSS

Now that we have settled on a solution, it's time to make the use as painless as possible. We are using TailwindCSS because it makes development faster and simpler, so a first approach could be to just define and use these formulas in the tailwind config

/tailwind.config.js (excerpt)
fontSize: 'h1': 'clamp(1.25rem, calc(1.375rem + (1.75 - 1.25) * ((100vw - 40rem) / (64 - 40))), 1.75rem)', 'h2': 'clamp(1.1rem, calc(1.2rem + (1.2 - 1.1) * ((100vw - 40rem) / (64 - 40))), 1.2rem)', 'base': 'clamp(1rem, calc(1.05rem + (1.05 - 1) * ((100vw - 40rem) / (64 - 40))), 1.05rem)', },
A primitive approach to adding fluid font-sizes to TailwindCSS

There are two things I don't like about this solution:

  • No fallback for older browsers, meaning any browser without support for clamp will simply show any h1 at 16px on all screen sizes

  • We need to manually create the formula for each case and paste it in there

Our developers shouldn't have to use and edit a complex formula anytime a font-size changes, as that is both annoying and error-prone. Wouldn't it be much nicer if we only had to set the smallest and biggest font size per style and let TailwindCSS do the rest?

Adding a simple plugin

Luckily TailwindCSS is built to make it easy to extend and add your own functionality. First let's change the font size definitions so we can enter a min and max value for each style. This will make changes to the design much easier in the future.

Next we will add our own tailwind plugin to automatically create our styles. In there we define the elements we want to cover (styles) and a single breakpoint to use for our fallback. The addBase function then adds our CSS to Tainwind's base layer.

As a first step lets create the fallback values for older browsers:

/tailwind.config.js (excerpt)
const plugin = require('tailwindcss/plugin') module.exports = { ... theme: { fontSize: 'h1-min': '2rem', 'h1-max': '3rem', }, }, plugins: [ plugin(function({ addBase, theme }) { const styles = ['h1'] const midScreen = '48rem' styles.forEach(style => { addBase({ [`${style}]: { fontSize: theme('fontSize.' + `${style}` + '-min'), [`@media (min-width: ${midScreen})`]: { fontSize: theme('fontSize.' + `${style}` + '-max'), } } }) }) }) ], }
Setting up the Tailwind plugin to create basic fallback rules for old browsers

This gets us the following output which will give some basic styling to older browsers. Small screens get the minimum font size, everything above the breakpoint get the maximum one. Not perfect but better than nothing.

/public/css/tailwind.css (excerpt)
h1 { font-size: 2rem; } @media (min-width: 48rem) { h1 { font-size: 3rem; } }
The output of our fallback rules

Adding the fluid typography formula

Now we can add the actually functionality we aim to create, by adding our formula to the component layer. Since we are now reusing the font-sizes from our config, I have moved them into variables which are added to the formula both with the rem unit attached and without, using parseFloat() to strip out the non-numeric characters.

/tailwind.config.js (excerpt)
plugin(function({ addBase, addComponents, theme }) { const styles = ['h1'] const screenMin = '40rem' // start of fluid font size const screenFB = '48rem' // fallback breakpoint const screenMax = '64rem' // end of fluid font size styles.forEach(style => { let fsMin = theme('fontSize.' + `${style}` + '-min') let fsMax = theme('fontSize.' + `${style}` + '-max') addBase({ [`${style}, .${style}`]: { fontSize: fsMin, [`@media (min-width: ${screenFB})`]: { fontSize: fsMax, } } }) addComponents({ [`${style}`]: { fontSize: `clamp(${fsMin}, calc(${fsMin} + (${parseFloat(fsMax)} - ${parseFloat(fsMin)}) * ` + `((100vw - ${parseFloat(screenMin)}rem) / (${parseFloat(screenMax)} - ${parseFloat(screenMin)}))), ${fsMax})` } }) }) })
The complete code of our Statamic fluid typography plugin

And we are done. Here are the final classes created by our plugin, which give us our fluid font-sizes using clamp() as well as a simple fallback for older browsers.

/public/css/tailwind.css (excerpt)
h1 { font-size: 2rem; } @media (min-width: 48rem) { h1 { font-size: 3rem; } } h1 { font-size: clamp(2rem, calc(2rem + (3 - 2) * ((100vw - 40rem) / (64 - 40))), 3rem); }
Finished output of our simple TailwindCSS fluid typography plugin

Any browser that can't work with clamp() will simply ignore the last statement and revert to the previous style definitions. By adding font-sizes for all other text-styles, this setup can easily serve as a boilerplate for our future projects, meaning we only have to change the minimum and maximum font-sizes for each new project.

Disclaimer: This plugin was a quick first solution to our problem. I have since changed a couple of things and turned it into a slightly more complex covering more use-cases. Feel free to use this as a starting point for your own plugin however and adapt it to exactly your workflow.

More Posts