Sass and Responsive Typography

James Steinbach
Share

As web fonts have grown (in number, quality, and ease of use), web typography has become a necessary part of web design. Web typography is a complex topic including many smaller aspects like font pairing, performance, FOUT, ligatures, and type scales.

As you make typographic choices when building websites, it is essential to keep responsive design in mind. It’s frustrating to read a page on a phone with huge desktop-sized headings; it’s also dull to see all the font sizes on a site stuck between 14px and 20px on a wide-screen monitor. These problems have helped me define responsive web typography with three components:

  1. Base font-size that changes at breakpoints
  2. Vertical rhythm that changes at breakpoints
  3. Consistent type scales that change at breakpoints

These three things are an important part of normal web typography: making them adjustable at a given breakpoint gives us the ability to tailor typography to the user’s viewport.

Organizing Data with Sass Maps

If you’ve never used a map in Sass before, this article is a great introduction. In Sass, maps are arrays of data. You can nest maps for detailed, multi-layer data organization. Let’s take a look at a map we can base responsive typography on:

$rwd-typography: (
  default: (
    base-font-size: 16px,
    vertical-rhythm: 1.414,
    type-scale: 1.2,
    min-width: null
  ),
  medium: (
    base-font-size: 18px,
    vertical-rhythm: 1.5,
    type-scale: 1.414,
    min-width: 480px
  ),
  large: (
    base-font-size: 20px,
    vertical-rhythm: 1.618,
    type-scale: 1.5,
    min-width: 960px
  ),
  xlarge: (
    base-font-size: 24px,
    vertical-rhythm: 1.618,
    type-scale: 1.618,
    min-width: 1300px
  )
);

That map has the three typographic details we need for responsive typography spread across four breakpoints. It also has a min-width measurement for the media query used at each breakpoint. Note: the media query value can be whatever your project needs. After all, breakpoints should be determined by content, not by framework defaults!

The base-font-size value in each nested map sets the normal font-size for body text. The vertical rhythm value sets the line-height for normal body text and is used to create vertical rhythm for headings and other non-<p> elements. The type-scale value determines how much larger each step up the type scale gets. For example, on our default size, paragraph text will be 16px, the next size up (let’s call it “block quote” for now) will be 16px × 1.2, or 19.2px. The next size up “subheading” perhaps?) will be 19.2px × 1.2, or 23px.

Now that I’ve introduced paragraph, sub-sub-heading, and sub-heading (and presumably, heading and hero sizes too), we’ll need to generate those sizes. Let’s put our type scale labels into a Sass list:

$rwd-scale-labels: (p, bq, sh, h, hero);

Now that we’ve got all our data stored, let’s write some Sass to build our sizes automatically!

Generating Sizes with Sass Loops and Mixins

We can use some loops and mixins to generate responsive font-sizes now. We’ll start by writing a loop that goes through each of the font scale labels and generates a Sass placeholder selector for each one.

@each $label in $rwd-scale-labels {
  %#{$label} {
    // we’ll put more code here soon…
  }
}

Inside each of those placeholders, we’ll nest another @each statement to loop through the breakpoints and generate our typography:

@each $breakpoint, $data in $rwd-typography {
  // $breakpoint represents the breakpoint’s key,
  // $data is the nested map containing its data
}

Inside that loop, the first thing we’ll do is find out if we need a media query or not. The default / first breakpoint doesn’t need a media query; the others do.

@if map-get($data, min-width) != null { //CHECK != syntax
  @media screen and (min-width: map-get($data, min-width)) {
    // generate CSS output here
  }
} @else {
  // generate CSS output here
}

Let’s leave all that nested code alone for now and write some helper functions to generate our CSS values. The first function we’ll write will get the font-size for the current label at the current breakpoint:

@function rwd-generate-font-size($label, $breakpoint) {
  $label-position: index($rwd-scale-labels, $label);
  $breakpoint-base-font-size: map-get(map-get($rwd-typography, $breakpoint), base-font-size);
  $breakpoint-type-scale: map-get(map-get($rwd-typography, $breakpoint), type-scale);
  $return: $breakpoint-base-font-size;
  @for $i from 1 to $label-position {
    $return: $return * $breakpoint-type-scale;
  }
  @return $return;
}

There are several advanced Sass functions used above:

  • index($item, $list) gets the position of an item in a list
  • map-get($map, $key) returns the value that matches the specified key in a given map

The @for loop roughly mimics a power() function / exponential math.

We’ll plug that into our loop right above our media query check:

$current-font-size: rwd-generate-font-size($label, $breakpoint);

Now, let’s expand the function above to generate the line-height to match our vertical rhythm:

@function rwd-generate-font-size($label, $breakpoint) {
  $label-position: index($rwd-scale-labels, $label);
  $breakpoint-base-font-size: map-get(map-get($rwd-typography, $breakpoint), base-font-size);
  $breakpoint-type-scale: map-get(map-get($rwd-typography, $breakpoint), type-scale);
  $breakpoint-vertical-rhythm: map-get(map-get($rwd-typography, $breakpoint), vertical-rhythm);
  $font-size: $breakpoint-base-font-size;
  @for $i from 1 to $label-position {
    $font-size: $font-size * $breakpoint-type-scale;
  }
  $base-vertical-rhythm: $breakpoint-base-font-size * $breakpoint-vertical-rhythm;
  $line-height: round($font-size / $base-vertical-rhythm) * $base-vertical-rhythm / $font-size;
  $return: join($font-size, $line-height);
  @return $return;
}

Now the $return value of this function is a list with two values: the font-size and the line-height. We’ll need to update the variable assignment in our loop to match that:

$generated-values: rwd-generate-font-size($label, $breakpoint);
  $font-size: nth($generated-values, 1);
  $line-height: nth($generated-values, 2);

The nth() function in Sass returns the list value at the given index position.

Now let’s put everything in our loop together:

@each $label in $rwd-scale-labels {
  %#{$label} {
    @each $breakpoint, $data in $rwd-typography {
      $generated-values: rwd-generate-font-size($label, $breakpoint);
      $font-size: nth($generated-values, 1);
      $line-height: nth($generated-values, 2);

      @if map-get($data, min-width) != "null" {
        @media screen and (min-width: map-get($data, min-width) {
          font-size: $font-size;
          line-height: $line-height;
        }
      } @else {
        font-size: $font-size;
        line-height: $line-height;
      }
    }
  }
}

Now to use those styles, extend them from actual selectors. For example:

body {
  @extend %p;
}

blockquote {
  @extend %bq;
}

h1 {
  @extend %h;
}

h2 {
  @extend %sh;
}

.hero-title {
  @extend %hero;
}

Now each of those selectors gets all the media-query based output we generated with our loops and function above! You can set all your data in the initial map and list, then extend the labels you created to create responsive typography with the automatically generated CSS output from the loops.