Skip to main content

Responsive typography with Sass maps

Igor Lakic

There is nothing better thing in a coder’s life than a clean, non-repetitive code that is easy to read, use, and reuse.

It is not an easy feat efficiently structuring your code and even more so when dealing with responsive designs. Amid the whole “mess” called responsive design, typography may well be the most important puzzle of every website mosaic yet, while simultaneously often being underplayed or at best not discussed as frequently as it should. For this reason, this article offers personal insights into responsive typography and my way of dealing with it in an efficient and concise manner.

Intro

Typically, you can make typography responsive with the use of vh and vw units, some Js libraries, or by using old style method with font pixel values for every breakpoint. These options have their strengths and weaknesses: vh/vw units still doesn't react well in all browsers (one of a few issues is that font sizes scale rapidly from one extreme to the other), Js libraries are not an optimal way because (some users nowadays already use some kind of Js blockers alongside a variety of Ad blockers), and plain pixel values do not have much sense these days as they are not scalable as for example rems (if you want to quickly scale an entire project you can do that easily if you're using rems by changing the root/base font size of the project).

Because of that I've tried to reach an optimal solution - it should be readable in most browsers but at the same time easy to read, use and reuse.

Sass maps to the rescue

A more developer and browser-friendly approach can be reached by using Sass maps.

First of all, I like to use rem units for font sizes and for most of the other properties like widths, heights, margins, paddings etc. I also like to use em units for breakpoints. This proved to be the best, most stable solution across various browsers (and you can also read more about that in this article by Zell).

So, having that in mind, it would be optimal to split values for various text elements (h1-h6 and p) into Sass maps. That way we could have all font sizes in one place which should make any future corrections of typography easily changeable, with minimal effort. But we could also map all our breakpoints into Sass maps too. That way we can combine those two and get a responsive typography system.

We also cannot forget line heights which are important for vertical rhythm or in other words, we would definitely like to make our website easier to read. ;)

That is one more variable we should add to our "mix".

Let us start with breakpoints.

$breakpoints: (
  'small' : 480px,
  'normal': 720px,
  'wide'  : 960px,
  'large' : 1280px
);

In the example above, we defined some values for breakpoints in Sass map that will be later used with our font sizes for different elements. Breakpoint names (key names from breakpoints Sass map) that are mapped in this map will become keys in Sass maps in which we declared values for various elements (p, h1-h6).

We can now define our values for p element and appropriate line heights that we would like to see defined in "unitless" values.

$p-font-sizes: (
  null  : (18px, 1.3),
  small : (19px, 1.5),
  normal: (20px, 1.4),
  wide  : 21px,
  large : 21px
);

The same process of making maps for h1-h6 elements can be applied.

"null" key represents base font size that are not in media query. Because of the mobile-first approach, all breakpoints are listed in ascending order.

Pixels into rems & ems

Now, you noticed that I'm using px units in these Sass maps for font sizes and there is one very simple explanation for that. I think in pixels but like I already wrote, I prefer using ems and rems. For easier use and automatic conversion to ems & rems, I use these 3 Sass functions:

// Strip units from a value
// ----------------------------
@function strip-units($value) {
  @return $value / ($value * 0 + 1);
}

// Calculate rems from a px value
// ------------------------------
@function rem-calc($px, $base-font-value: $base-font-size) {
  @if not unitless($px) {
    $px: strip-units($px);
  }
  @if not unitless($base-font-value) {
    $base-font-value: strip-units($base-font-value);
  }
  @return ($px / $base-font-value) * 1rem;
}

// Calculate ems from a px value
// ------------------------------
@function em-calc($px, $base-font-value: $base-font-size) {
  @if not unitless($px) {
    $px: strip-units($px);
  }
  @if not unitless($base-font-value) {
    $base-font-value: strip-units($base-font-value);
  }
  @return ($px / $base-font-value) * 1em;
}

In the functions above, there is a variable base-font-size in which default base font size is specified. It is very important to define it for successful unit conversion!

$base-font-size: 16px;

Sass mixin for responsive typography

We can write the main Sass mixin which will connect all these pieces:

@mixin font-size($font-size-map, $font-size-breakpoints: $breakpoints) {
  @each $font-size-breakpoint, $final-font-size in $font-size-map {
    @if $font-size-breakpoint == null {
      @include make-font-size($final-font-size);
    }
    @else {
      // Use the value if $final-font-size is a key that exists in
      // $font-size-breakpoints
      @if map-has-key($font-size-breakpoints, $font-size-breakpoint) {
        $font-size-breakpoint: map-get($font-size-breakpoints, $font-size-breakpoint);
      }
      @media screen and (min-width: em-calc($font-size-breakpoint)) {
        @include make-font-size($final-font-size);
      }
    }
  }
}

@mixin make-font-size($final-font-size) {
  // Include both font-size and line-height if $final-font-size is a list
  @if type-of($final-font-size) == "list" {
    font-size: nth($final-font-size, 1);
    font-size: rem-calc(nth($final-font-size, 1));
    @if (length($final-font-size) > 1) {
      line-height: nth($final-font-size, 2);
    }
  }
  @else {
    font-size: $final-font-size;
    font-size: rem-calc($final-font-size);
  }
}

There are a lot of things going on in this Sass mixin. I'll try to explain it a bit. First of all, this mixin takes all the values from maps where we placed breakpoints and if there are some values that we put as keys that are not in the breakpoint map, it will use that value. In other words, this mixin is going to loop and use values stored in Sass maps.

But the interesting part is that if you choose to put another breakpoint for some text element (p, h1-h6), then it will also respect that and use that value as a breakpoint even if we did not placed it earlier in the Sass map for breakpoints.

$p-font-sizes: (
  null  : (18px, 1.3),
  small : (19px, 1.5),
  normal: (20px, 1.4),
  900px: 20.5px,
  wide  : 21px,
  large : 21px
);

It will also convert all pixel values from Sass maps and convert them into ems and rems. Font sizes are going to be in rems and breakpoint in ems. Almost all modern browsers natively support ems and rems, yet I implemented the rem to px conversion fallback function anyway before the rem-calc instances.

Unitless values that are written next to font sizes are going to be interpreted as values for line heights.

And that is all to it!

This way you got all those general font values that are used throughout your website in one place. And to use it you just call a corresponding mixin like this:

@include font-size($p-font-sizes);

Conclusion

I originally wrote and started using this mixin years ago when rem and em units were not nearly as well-supported as they are today (they are supported in 98.87% of browsers as of March 2020). Therefore, you can decide to use this logic directly with rem and em units, without fallbacks in pixels and without calculations from pixels to rems/ems. I will leave that to your preferences and needs. :)
Anyways, Sass maps are a really powerful tool for organizing code which can find its use in many more scenarios, such as for organizing colors, icons, media queries, structuring animations and many more.

I hope you found this short tutorial helpful and that these examples will also find their use in your projects as well. The code demonstrated here can also be found in my Github repo, so feel free to share it and maybe even reward it with some Github stars. ;)

Until next time, have a nice coding day! :)