Fluid typography with CSS clamp

Learn to create a simple, accessibility friendly and configurable fluid type system that uses modern CSS sizing functions.

I’m a big fan of clamp()—it’s decent at doing what I like to do the most with CSS: let the browser do its job with some hints at how to do it. It also provides just the right amount of control, which is handy for layout elements, too.

In this tutorial, we’re going to use clamp to generate a little fluid type system that can be configured using CSS Custom Properties.

Getting started permalink

All we need for this tutorial is a little HTML page and a CSS file. Go ahead and create the following files:

  1. index.html
  2. global.css

Now, inside index.html, add the following:

Code language
<!DOCTYPE html>
<html lang="en">
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Fluid type demo</title>
    <link rel="stylesheet" href="global.css" />
    <article class="[ post ] [ flow ]">
      <h1>Fusce dapibus, tellus ac cursus commodo</h1>
      <h2>Donec ullamcorper nulla non</h2>
      <h3>Morbi leo risus, porta ac consectetur</h3>
      <p>Morbi leo risus, porta ac consectetur ac, vestibulum at eros. Donec sed odio dui. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Fusce dapibus, tellus ac cursus commodo, tortor mauris condimentum nibh, ut fermentum massa justo sit amet risus. Vestibulum id ligula porta felis euismod semper.</p>

That’s it for HTML. It’s some lipsum placeholder text to demonstrate our fluid type system. Job done!

Digging in to the CSS permalink

Now for the fun part. Let’s add our basic global styles first. Open up global.css and add the following to it:

Code language
body {
  background: #f3f3f3;
  color: #252525;
  line-height: 1.5;
  font-family: Georgia, serif;
  padding: 2rem;

h3 {
  font-family: -apple-system, BlinkMacSystemFont, avenir next, avenir, helvetica neue,
    helvetica, Ubuntu, roboto, noto, segoe ui, arial, sans-serif;
  line-height: 1.1;
  font-weight: 900;

That’ll make things look a little nicer. We’re using my favourite font, Georgia, as our base and the system font stack for headings. This contrast will really help you see the fluid type in action.

Now we can add the smart stuff—the fluid type setup. Open up global.css and add the following to it:

Code language
p {
  font-size: clamp(
    var(--fluid-type-min, 1rem),
    calc(1rem + var(--fluid-type-target, 3vw)),
    var(--fluid-type-max, 1.3rem)

There’s a lot going on here, so let’s break it down.

The clamp() function takes a minimum value, an ideal value and a maximum value. This allows us to create some locks.

To power all of this, we’re using 3 custom properties:

  1. --fluid-type-min is the smallest we will allow our text to go
  2. --fluid-type-target is our ideal, fluid setting. We use calc() because if you just use a viewport unit to size your type, it can cause problems in zooming, which in turn, creates a WCAG accessibility failure.
  3. --fluid-type-max is the largest we will allow our text to go

For all three custom properties, we are setting the default value as the second parameter. This means that you can drop this fluid type system into any project, and even if none of those properties are defined: the system will still work off those default, sensible values.

Implementing our system permalink

We have applied our fluid type system to the following elements: h1, h2, h3, p. We could—if we wanted—turn this into a utility class for maximum portability. For this tutorial, we’ll keep it simple with type selectors though.

We want to add some specific settings for each of these, using custom properties, or all the text will be the same size.

Open up global.css and add the following to it:

Code language
h1 {
  --fluid-type-min: 2.5rem;
  --fluid-type-max: 5rem;
  --fluid-type-target: 5vw;

  max-width: 15ch;

h2 {
  --fluid-type-min: 1.8rem;
  --fluid-type-max: 3rem;

h3 {
  --fluid-type-min: 1.5rem;
  --fluid-type-max: 2.5rem;

h3 {
  max-width: 30ch;

p {
  max-width: 60ch;

For the <h1>, we increase the --fluid-type-target to a larger, 5vw. By increasing the viewport unit, we speed up the rate of growth, which will help to maintain its extra large size. To reduce the rate of growth and have less difference between your minimum and maximum sizes: reduce the size of --fluid-type-target.

For all of the other elements, the default growth rate is fine, so all we’re doing is setting sensible minimum and maximum sizes, using standard rem units.

We’re done! You can see a live demo of what we have built, here. You can also download the completed source files, here.

Wrapping up permalink

This is a very simple, bare-bones system and will comfortably support a lot of usecases. For more advanced, complex designs, I would recommend using something like Typetura which gives very fine control or Utopia, which is a level up from this approach that we’ve learned today.

For full disclosure, I’m not a huge fan of fluid type, personally. I’ve had such a mixed, checkered history with it over the years, and personally, I prefer to create a size scale, implemented via media queries. That’s what this site does, at the time of writing.

Fluid type is in demand, too, so what you’ve learned today will undoubtedly be useful at some point—if nothing else, to give you a useful context of how clamp() works.

Until next time, take it easy 👋

Big thanks to Eric Bailey for casting his expert eye on this for me.