Skip to main content

Fluid Typography with CSS & SASS

Front-end Development

Since responsive websites became the standard, I've always toyed with fluid typography. In the beginning, the best we could do were javascript solutions targeting one-off banner/hero areas of the site. Solutions like FitText came in handy for this, but when the goal is to scale all text, it fell short.

Fast forward a bit, I experimented with adjusting font size on the html element, adding media queries and using rem units on all child elements that need to scale. This scaled the text in a stepped fashion. It was written something like this...


html {
  font-size: 0.75rem; /* 12px */
}

@media (min-width: 600px) {
  html {
    font-size: 0.875rem; /* 14px */
  }
}

@media (min-width: 920px) {
  html {
    font-size: 1rem; /* 16px */
  }
}

h2 {
  font-size: 3rem;
}

The h2 element would be 36px, 42px and 48px. In practice, this was hard to maintain on a large site, it got complex very quickly and it's not truly fluid. There were always elements like paragraph text, category tags or subscripts that would scale too small in narrow viewport widths. This would result in having to set font sizes for narrow viewports to prevent unreadable text. The pitfalls were many, so I let the obsession go for a while. Well, now there's a great way to achieve true fluid typography with the CSS clamp function.

CSS Clamp() function

Clamp is a CSS function that allows you to set a minimum, maximum and preferred value. It can be used anywhere length, frequency, angle, time, percentage, number, or integer values are allowed, like width, padding, margin and font-size rules. It looks something like this:


  font-size: clamp(16px, 2.5vw, 48px); 

The three parameters are minimum value, preferred value, and maximum value. The clamp() function is a shorthand for two other functions: min() and max(). I won't go into how they work here. That's already been covered elsewhere. But I will pass along a SASS mixins that can get you experimenting with this feature without all the complexity.

Clamp meets Sass

Dart Sass has deprecated the slash as a division operator, so I've updated the mixins accordingly. In addition, you'll have to include an @use at the top of the file.

Note: LibSass and Ruby Sass users can simply convert the math.div() to an equation with a slash. 

@use "sass:math";

I like to work in pixels, so we'll need some helper functions to allow us to use pixel units in the mixin parameters.

@function strip-units($number) {
  @return math.div($number, ($number * 0 + 1));
}

@function calculateRem($size) {
  $remSize: math.div($size, 16px);
  @return #{$remSize}rem;
}

Those set up a function we use to output the clamp calculations. All parameters should be pixels. There is an added parameter $preferred_viewport_width which is the viewport width you want the $preferred size to be. This will help you match those desktop mockups at a certain viewport width.

@function calculateClamp(
  $minimum,
  $preferred,
  $maximum,
  $preferred_viewport_width: 1000px
) {
  $clamp_parameter_1: calculateRem($minimum);
  $clamp_parameter_2: strip-units((math.div(($preferred + 0), $preferred_viewport_width)) * 100) * 1vw;
  $clamp_parameter_3: calculateRem($maximum);

  @return clamp(#{$clamp_parameter_1}, #{$clamp_parameter_2}, #{$clamp_parameter_3});
}

Next, we use the function to create a reusable mixin...

/* The css property will be at the preferred size on 1000 pixel wide viewports by default. */
@mixin pixelsToResponsiveUnit(
  $property_name,
  $minimum,
  $preferred,
  $maximum,
  $preferred_viewport_width: 1000px
) {
  #{$property_name}: calculateClamp($minimum, $preferred, $maximum, $preferred_viewport_width);
}

And it can be used like this...

h2 { 
 @include pixelsToResponsiveUnit(font-size, 24px, 38px, 60px)
}

One thing leads to another

Using this is in practice, I quickly realized I needed to incorporate vertical gutter spacing too. As headers shrink so should the white space between page elements, keeping a proportional vertical rhythm.

Add in another mixin to handle my two most common vertical gutters...


@mixin responsive-gutter-1x($direction: bottom, $gutter: 36px) {
  @if $direction == top {
    margin-top: $gutter;
    margin-top: calculateClamp(math.div($gutter, 2), ($gutter * 0.75), $gutter);
  } @else if $direction == bottom {
    margin-bottom: $gutter;
    margin-bottom: calculateClamp(math.div($gutter, 2), ($gutter * 0.75), $gutter);
  } @else {
    @error "Unknown direction #{$direction}.";
  }
}

@mixin responsive-gutter-2x($direction: bottom, $gutter: 36px) {
  @if $direction == top {
    margin-top: ($gutter*2);
    margin-top: calculateClamp($gutter, ($gutter * 1.5), ($gutter*2));
  } @else if $direction == bottom {
    margin-bottom: ($gutter*2);
    margin-bottom: calculateClamp($gutter, ($gutter * 1.5), ($gutter*2));
  } @else {
    @error "Unknown direction #{$direction}.";
  }
}

An example of this SASS looks something like this...

h2 {
  @include pixelsToResponsiveUnit(font-size, 24px, 38px, 60px)
  @include responsive-gutter-1x(top);
  @include responsive-gutter-2x(bottom);
}

A more advanced mixin would allow us to define baseline gutter sizes and use a multiplier for many gutter sizes...

@mixin responsive-gutter(
  $property:margin-bottom,
  $multiplier:1,
  $mobile-gutter: 10px,
  $tablet-gutter: 15px,
  $desktop-gutter: 20px) {

    @include pixelsToResponsiveUnit(
      #{$property},
      $mobile-gutter * $multiplier,
      $tablet-gutter * $multiplier,
      $desktop-gutter * $multiplier
    );
}

It could be used something like this: 

h2 {
  @include pixelsToResponsiveUnit(font-size, 24px, 38px, 60px)
  @include responsive-gutter(margin-top, 1);
  @include responsive-gutter(margin-bottom, 2);
}

To see it in action you can play around with it or fork my codepen.