17th October 2025

by Jesús Olano

Interactive Fluid Typography

What we call fluid typography are a set of tricks in CSS that allows to adapt the type size and leading when the screen size changes. So instead of having fixed breakpoints where the font-size abruptly, we want to have a smooth increase between different screen sizes, removing blind spots near the breakpoints where the font was disproportionally big or small.

The most basic implementation of fluid typography is to take a base size and a base screen width. If we divide the screen size (100vw) by the base size, we will have a ratio that will tell us how big is the screen related to the base size. Until recently, we had to take care in order not to multiply or divide two numbers with units, as this created an error. This has been solved in Chrome 140, creating exciting possibilities as outlined in this post by Amit Sheen, but support is still not baseline as I write this lines, so we will then just have to take care using unitless numbers for our base-screen-size and our base-font-size, in order to be able to operate with vw. This point is important and we'll come back to it later.

--base-screen-size: 1200;
--base-font-size: 16;
--typographic-ratio: 1.618;

.font-size-base {
  calc(
    var(--base-font-size) * (100vw / var(--base-screen-size))
  )
}

.font-size-md {
  calc(
    var(--font-size-base) * var(--typographic-ratio)
  )
}

.font-size-lg {
  calc(
    var(--font-size-md) * var(--typographic-ratio)
  )
}

With this setup, we can have a text-size equal to 16px in our desired base-size that will grow and shrink linearlly with the size of our viewport. This has the obvious drawback that size will grow and shrink indefinetely, making it not very usable as it is. We could restrict that behaviour with @media-queries, setting it fluid between two breakpoints, and setting it fixed on the rest, but fortunately modern CSS allows to write this in a much more elegant way.

Clamp to the rescue

With clamp() function, we can directly set a minimum and maximum size and setting it fluid in between. This method is a good approach, and we could even set different type sizes taking the same value as base and multiplying this value by your favourite typographic ratio—like for example the Golden Ratio (1.618).

  --base-screen-size: 1200;

  --base-font-size-min: 14;
  --base-font-size: 15;
  --base-font-size-max: 16;

  --typographic-ratio: 1.618;

  .font-size-base {
    font-size: clamp(
      calc(1px * var(--base-font-size-min)),
      calc(var(--base-font-size) * 100vw / var(--base-screen-size)),
      calc(1px * var(--base-font-size-max)),
    )
  }

  --font-size-md {
    font-size: calc(var(--font-size-base) * var(--typographic-ratio))
  }

  --font-size-lg {
    font-size: calc(var(--font-size-md) * var(--typographic-ratio))
  }

However this method has the drawback that for small screens we generally want to set smaller jumps between our different typesizes, so ideally we would set a different typographic ratio. In order to do that we would have to create a breakpoint where all the types would change abruptly, making that part of the design a little bit junky. Also, it is not very obvious at between which viewport widths is the fluid part going to kick in.

Fluid scale generator

In order to overcome this, Andy Bell points out in its phenomenal course Complete CSS a way to generate a typographic scale setting different ratios between screen sizes. So for example, we can create a scale for screens up until 400px that will have a 1.414 ratio, and then from screens from 400px to 1200px the ratio will increase the bigger the viewport, until a maximum of 1.618. In order to do that, we can use the values generated by Utopia website. As Utopia already generates the fluid values for each of our steps, we just have to generate then and paste them in our code.

  --step-0: clamp(0.875rem, 0.8125rem + 0.25vw, 1rem);
  /* Step 1: 19.796px → 25.888px */
  --step-1: clamp(1.2373rem, 1.0469rem + 0.7615vw, 1.618rem);
  /* Step 2: 27.9915px → 41.8868px */
  --step-2: clamp(1.7495rem, 1.3152rem + 1.7369vw, 2.6179rem);
  /* Step 3: 39.58px → 67.7728px */

  .font-size-base {
    font-size: var(--step-0);
  }

  .font-size-md {
    font-size: var(--step-1);
  }

  .font-size-lg {
    font-size: var(--step-2);
  }

Now we are in control of everything, we can define the minimum and maximum sizes where our fluid typography will work, and we can also define a different scale for small viewports what will steadily grow—or shrink if that is desired—until reaching the scale we set for larger viewports. Also, it allows us to use rem instead of px, which should be considered a best practice for accesibility. This makes our typography fit and look harmonious in every screen size.

However, there is a price we are paying, we're losing all control in our CSS, and each time we want to change our ratios, add and/or remove different steps or setting different screen limits, we will have to calculate back at Utopia's website and paste it in our code. Also, we lose the ability to try different values in our CSS and see them updated live in our browser.

The missing piece: Typed Arithmetic

As we introduced before, CSS Typed Arithmetic shipped in Chrome 140, allowing us “to write expressions in CSS such as calc(10em / 1px) or calc(20% / 0.5em * 1px)”. This small addition is crucial, as now we can change our unitless typographic ratio based on screen width. Taking as a base the slope formula that Utopia also uses, we will calculate a --screen-normalizer variable that will allow us to adjust both our font-size and our typographic-ratio to the viewport size withing our predefined bounds.

--base-font-size-small: 14px;
--base-font-size-large: 16px;

--lower-ratio: 1.414;
--upper-ratio: 1.618;

--lower-bound: 400px;
--upper-bound: 1200px;

--screen-normalizer: clamp(
  0,
  (100vw - var(--lower-bound)) / (var(--upper-bound) - var(--lower-bound)),
  1
);

--fluid-base-size: calc(
  var(--base-font-size-small) +
  (
    var(--base-font-size-large) -
    var(--base-font-size-small)
  ) *
  var(--screen-normalizer)
);

--fluid-step: calc(
  var(--lower-ratio) +
  (
    var(--upper-ratio) -
    var(--lower-ratio)
  ) *
  var(--screen-normalizer)
);

.font-size-base {
  font-size: var(--fluid-base-size);
}

.font-size-md {
  font-size: calc(var(--font-size-base) * var(--fluid-step))
}

.font-size-lg {
  font-size: calc(var(--font-size-md) * var(--fluid-step))
}

This opens a whole world of possibilities, as now we can easily switch between different type scales and sizes live from your console, which we find really handy for prototyping. Having all your spacing relative to just two measures makes it trivial to generate different additional type or space sizes when needed. In case I need a font size a half-step bigger or smaller than any of my current sizes, we just have to multiply or divide by the square root of my --fluid-step variable.

--fluid-base-size: calc(
  var(--base-font-size-small) +
  (
    var(--base-font-size-large) -
    var(--base-font-size-small)
  ) *
  var(--screen-normalizer)
);

--fluid-step: calc(
  var(--lower-ratio) +
  (
    var(--upper-ratio) -
    var(--lower-ratio)
  ) *
  var(--screen-normalizer)
);

--fluid-step-half: pow(var(--fluid-step), 0.5);

.font-size-sm {
  font-size: calc(var(--fluid-base-size) / var(--fluid-step-half));
}

.font-size-base {
  font-size: var(--fluid-base-size);
}

.font-size-demi {
  font-size: calc(var(--fluid-base-size) * var(--fluid-step-half));
}

.font-size-md {
  font-size: calc(var(--font-size-base) * var(--fluid-step))
}

.font-size-lg {
  font-size: calc(var(--font-size-md) * var(--fluid-step))
}

As of today, this is still just supported in Chrome, but we can start using it having Utopia generated classes as fallback using @supports rules.

--step-0: clamp(0.875rem, 0.8333rem + 0.2222vw, 1rem);

.font-size-base {
  font-size: var(--step-0);
}

@supports (transform: scale(calc(1px / 1px))) {
  --fluid-base-size: calc(
    var(--base-font-size-small) +
    (
      var(--base-font-size-large) -
      var(--base-font-size-small)
    ) *
    var(--screen-normalizer)
  );

  .font-size-base {
    font-size: var(--fluid-base-size);
  }
}

The future: CSS functions

While already in the baseline support, CSS functions still don't support some mathematical functions as pow() or clamp(). When that time comes, it will be even easier to make font and space sizes on the fly, using just a function and a number that expresses the number of steps on your design system. For example, --fluid-size(2) could be used to get the size for the second step in the scale and work both for fonts and for spacing.

@function --fluid-scaler(--small-measure, --large-measure) {
  result: calc(
    --small-measure +
    (
      --large-measure -
      --small-measure
    ) *
    var(--screen-normalizer)
  );
}

--fluid-base-size: --fluid-scaler(
  var(--base-font-size-small),
  var(--base-font-size-large)
);

--fluid-step: --fluid-scaler(
  var(--lower-ratio),
  var(--upper-ratio)
);

@function --fluid-size(--step) {
  result: calc(
    var(--fluid-base-size) *
    var(--fluid-step) ^
    var(--step)
  );
}

.font-size-base {
  font-size: --fluid-size(1);
}

.font-size-md {
  font-size: --fluid-size(2);
}

Meanwhile, pressing the button below will open a menu that will let you play with all the typographic measures of this website, squizzing and expanding it as much as you like!