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

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 $number / ($number * 0 + 1);
}

@function calculateRem($size) {
  $remSize: $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
) {
  @return clamp(
    calculateRem($minimum),
    strip-units((($preferred + 0) / $preferred_viewport_width) * 100) * 1vw,
    calculateRem($maximum)
  );
}

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


/* The font will be at the preferred size on 1000 pixel wide viewports by default. */
@mixin pixelsToFluidText($minimum, $preferred, $maximum, $preferred_viewport_width: 1000px) {
  /* Fallback for IE11 */
  font-size: calculateRem($preferred);
  /* Modern browsers */
  font-size: calculateClamp($minimum, $preferred, $maximum, $preferred_viewport_width);
}

And it can be used like this...


h2 { 
 @include pixelsToFluidText(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 proper 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(($gutter/2), ($gutter * 0.75), $gutter);
  } @else if $direction == bottom {
    margin-bottom: $gutter;
    margin-bottom: calculateClamp(($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}.";
  }
}

And the final SASS looks something like this...


  h2 {
    @include pixelsToFluidText(24px, 38px, 60px)
    @include responsive-gutter-1x(top);
    @include responsive-gutter-1x(bottom);
  }
  
  p {
    @include responsive-gutter-1x(bottom);
  }
  
  table, blockquote, fieldset {
    @include responsive-gutter-1x(top);
    @include responsive-gutter-1x(bottom);
  }

You can check out my codepen to see it in action.