Using Sass to Build a Custom Type Scale with Vertical Rhythm

James Steinbach
Share

One way to achieve visual consistency in web design is to use a vertical rhythm. For a website, this would mean that no matter what font-size any text element is, its line-height is always an even multiple of a consistent unit of rhythm. When this is done precisely, you could put a striped background behind your page and each text block (paragraphs, headings, blockquotes, etc) would line up along the lines in that grid.

As you could imagine, setting this up by hand would require a lot of math and typing. If you want to change the proportions of that grid responsively, you’ll redo that work for every breakpoint. Sass, as you might expect, provides a great toolbox to automate all that work and generate a custom type scale with consistent vertical rhythm more easily.

I’ll start off by admitting that there are already some good Sass plugins that help build a custom type scale with consistent vertical rhythm. If you’re just looking to grab a pre-built chunk of code, try Typesetting, Typomatic, or Typecsset.

Why Build Our Own?

That raises a great question: With all those tools available, why should we try building our own? There are a couple of reasons. The fact that there are three different tools already means that there are different ways to solve this successfully. Additionally, building our own type scale with Sass is a great way to move beyond the basics and learn some more advanced Sass.

Before we start, let’s define what kind of type scale we’re going to build. The scale will start with a base font-size and line-height. From that font-size, it will generate several proportionally larger font-size values. This Sass code will also control the leading for all the sizes it generates, allowing us to keep the line height tied to a multiple of the base line-height.

One last detail for fun: We’ll be able to adjust the base font-size and line-height for various screen widths with a short media query mixin.

Setting Up Variables

We’ll put these variables near the top of the file because we want to organize our code by keeping all the “config” stuff in one easy-to-find place.

If we didn’t want this to be responsive, this is all we’d need for the base font-size and line-height values:

$base-font-size: 16px;
$base-line-height: 1.3;

We’ll also use a variable for the proportion by which font-size values will scale up:

$scale: 1.5;

However, we’re going to make this responsive, so a Sass map is a better way to store this information:

$bp-sizes: (
  small: (
    base-font-size: 16px,
    base-line-height: 1.3,
    scale: 1.25
  ),
  medium: (
    base-font-size: 18px,
    base-line-height: 1.4,
    scale: 1.4
  ),
  large: (
    base-font-size: 22px,
    base-line-height: 1.5,
    scale: 1.5
  )
);

We’ll also need a set of breakpoint values for media queries. You could make the $bp-sizes map more complex and add a min-width value. But in this tutorial, I’ll allow the possibility that you may already have a breakpoint plugin you like, so we’ll keep those separate. Just make sure the breakpoint labels match:

$breakpoints: (
  small: 480px,
  medium: 960px,
  large: 1200px
);

In just a moment, we’ll look at how we use the values in that map. For now, let’s move on to the final set of variables we need — the list of font sizes we plan to use.

$font-sizes: ( p, bq, ssh, sh, h, hero );

In this case, we’re using p for paragraph, bq for blockquotes, ssh for sub-sub-heading, sh for sub-heading, h for heading, and hero for hero text.

Setting up Mixins

Now that we have all the data we need in variables, it’s time to start processing it. To get the generated CSS we desire, we’ll need a mixin that creates font-size and line-height properties for each label in the $font-sizes map. Then we’ll repeat that mixin with data from each breakpoint in the $bp-sizes map, wrapping each set of styles in the appropriate media queries.

Generating font-size and line-height

Here is our mixin to generate the font-size and line-height values:

@mixin generate-font-properties($label, $base-font-size, $base-line-height, $scale) {
  $scale-value: index($font-sizes, $label) - 1;

  $fs: $base-font-size * pow($scale, $scale-value);
  $lh: $base-font-size * $base-line-height / $fs;

  @while $lh < 1 {
    $lh: $lh + $lh;
  }

  font-size: $fs;
  line-height: $lh;
}

This mixin accepts four parameters: The label of the font size (from the $font-sizes map), the base font size, the base line height, and the scale (all from $bp-sizes).

The first line of this mixin gets the index of the label in the $font-sizes map and subtracts one so that the generated font sizes increment correctly.

Note: Sass index values start the count at 1, unlike most other programming languages which start index count at 0.

The second line sets the variable $fs to the current base font-size multiplied by the current scale to the power of the scale index. In other words, if it’s the first generated font size, $fs will be the base font size multiplied by 1 (the scale to the power of 0). If it’s the second generated font size, $fs will be the base font size multiplied by scale to the first power.

Note: If you’re using Compass, pow() is a helper function; if not, you may want to include a pow() function like this one.

After this, $lh (which serves as a placeholder for the eventual line height) is set to the base vertical rhythm (font size * line height) divided by the current calculated font size. This makes the starting line height for any font size identical to the base vertical rhythm. For larger font sizes, this measurement is smaller than 1. The @while loop increases the $lh variable by one vertical rhythm unit at a time until the line height is at least 1.

Note: We’re generating a unitless line-height value so that we can use any valid font size measurements in this mixin. You can use any measurement units you like (px, rem, em, vh) in the $bp-sizes map.

Finally, this mixin outputs the font-size as the $fs value and the line-height as the $lh value.

Generating Breakpoints

We’ll also need a mixin to generate media queries for breakpoints. Here’s what I like to use:

@mixin breakpoint($name) {
  @if not map-has-key($breakpoints, $name) {
    @warn "Invalid breakpoint `#{$name}`.";
  } @else {
      @if map-get($breakpoints, $name) == '' {
        @content;
      } @else {
        @media (min-width: map-get($breakpoints, $name)) {
        @content;
      }
    }
  }
}

This mixin checks the name of the breakpoint passed to it against the list of labels in the $breakpoints map. If the name is missing, it returns an error message.

If $name is in the $breakpoints map, the next @if statement looks for a measurement paired with the name. If there is no measurement value, the styles are compiled without a media query. This allows us to set a default set of font config values in the $bp-sizes map.

If $name has a measurement, that is used as the min-width media query value. (Yes, I’m a big believer in mobile-first media queries.) All the @content styles are wrapped in that media query.

Looping through Values and Breakpoints

Now that we have a working @mixin for generating font size and line height, we need to repeat it for all values in the $font-sizes list. To do that, we’ll use an @each loop:

@each $label in $font-sizes {
  %#{$label} {
    @include generate-font-properties($label, $base-font-size, $base-line-height, $scale);
  }  
}

All that’s left is to take one more step back and wrap that loop in a loop that goes through the $bp-sizes map. This loop will also include our breakpoint() mixin to generate media queries. To get the necessary information from the $bp-sizes map, this loop will use the get-breakpoint-property() helper function. It searches $bp-sizes, finds the breakpoint size map, and returns the right property value from that map:

@function get-breakpoint-property($prop, $bp) {
  @return map-get(map-get($bp-sizes, $bp), $prop);
}

@each $size, $data in $bp-sizes {

  $bsf: get-breakpoint-property(base-font-size, $size);
  $blh: get-breakpoint-property(base-line-height, $size);
  $s: get-breakpoint-property(scale, $size);

  @include breakpoint($size) {
    @each $label in $font-sizes {
      #{$label} {
        @include generate-font-properties($label, $bsf, $blh, $s);
      } 
    }
  }
}

The loop then calls the breakpoint() mixin to generate each necessary media query. Inside that mixin, it runs the $font-sizes loop and regenerates font placeholders using the appropriate font size, line height, and scale for the current breakpoint. The output is attached to placeholder selectors, which we then extend where needed:

.page-title {
  @extend %h;
}

You can see the code in action in this CodePen:

See the Pen Sass Type Scale with Vertical Rhythm by SitePoint (@SitePoint) on CodePen.

Conclusion

In the process of creating a Sass plugin that allows us to quickly generate custom font sizes with consistent vertical rhythm and responsive adjustments, we’ve learned several important things about Sass.

We’ve used variables, lists, and maps. We’ve used mixins, extends, and loops. These are some of the most important tools in your Sass toolbox. Please share in the comments how you could use this plugin, or how you can use these tools to create other useful styles!