Managing responsive typography with Sass

In my previous article I used the maps feature of Sass to create a simple interface that can DRY up the creation of colour variations and help to bridge the gap between developers and designers. This article covers a similar methodology for a designer’s favourite but many developer’s nightmare; managing typographic rhythm consistently across different screen sizes.

The first step is to define a set of named breakpoints and a simple interface for using them. You may already have a similar set of variables, functions and mixins in your codebase but if not then this is a serviceable implementation:

$breakpoints: (
  small:   480px,
  medium:  720px,
  large:   960px,
  x-large: 1280px
);
~/config/_breakpoints.scss
@function breakpoint($breakpoint-name) {
  $breakpoint-value: map-get($breakpoints, $breakpoint-name);

  @if $breakpoint-value {
    @return $breakpoint-value;
  }

  @warn "Breakpoint '#{$breakpoint-name}' not found in $breakpoints";
}
~/sass-lib/functions/_responsive.scss
@mixin respond-above($breakpoint-name) {
  $breakpoint-value: breakpoint($breakpoint-name);

  @if $breakpoint-value {
    @media screen and (min-width: $breakpoint-value) {
      @content;
    }
  }
}
~/sass-lib/mixins/_responsive.scss

The second step is the key to this technique and it’s just another map. The map of text sizes gives a name to each size, each size defines breakpoints and each breakpoint contains the font-size and line-height properties. Only the breakpoints that will change need to be defined.

If the base font size—set on the document element or body—will change then it’s best to avoid using em units (honestly, it’s fine to use px!) as calculating other font sizes against a changing baseline would cause unnecessary complexity. The font sizes and line heights used below are from A More Modern Scale for Web Typography.

$text-sizing: (
  centi: (
    small: (
      font-size: 12px,
      line-height: 16px
    )
  ),
  deci: (
    small: (
      font-size: 14px,
      line-height: 20px
    )
  ),
  base: (
    small: (
      font-size: 16px,
      line-height: 26px
    )
  ),
  deca: (
    small: (
      font-size: 18px,
      line-height: 26px
    ),
    large: (
      font-size: 20px,
      line-height: 30px
    )
  ),
  hecto: (
    small: (
      font-size: 22px,
      line-height: 28px
    ),
    large: (
      font-size: 24px,
      line-height: 32px
    )
  ),
  kilo: (
    small: (
      font-size: 24px,
      line-height: 32px
    ),
    large: (
      font-size: 28px,
      line-height: 36px
    )
  ),
  mega: (
    small: (
      font-size: 36px,
      line-height: 48px
    ),
    large: (
      font-size: 40px,
      line-height: 60px
    )
  )
);
~/config/_typography.scss

The text sizes map should be easy to read and modify but because of its size it’s unwieldy to use. The addition of a few functions to access individual text sizes and breakpoints makes it much easier to consume:

@function text-breakpoints-for ($text-size) {
  $text-breakpoints: map-get($text-sizing, $text-size);

  @if $text-breakpoints {
    @return $text-breakpoints;
  }

  @warn "Text size '#{$text-size}' not found in $text-sizing";
}

@function text-properties-for ($text-size, $breakpoint-name) {
  $text-breakpoints: text-breakpoints-for($text-size);
  $text-properties: map-get($text-breakpoints, $breakpoint-name);

  @if $text-properties {
    @return $text-properties;
  }

  @warn "Breakpoint '#{$breakpoint-name}' for text size '#{$text-size}' was not found";
}
~/sass-lib/functions/_typography.scss

The functions are only building blocks for the mixins below and probably won’t be used outside of them. There are two mixins; one for a singular font size and line height for a given breakpoint and a second that will loop over the requested text size and output all of its breakpoints. The latter also takes an argument for the default breakpoint, which in the example is set to the narrowest size because we should be building mobile first.

@mixin text-size ($text-size, $breakpoint-name: 'small') {
  $text-size-properties: text-properties-for($text-size, $breakpoint-name);

  @if $text-size-properties {
    font-size: map-get($text-size-properties, 'font-size');
    line-height: map-get($text-size-properties, 'line-height');
  }
}

@mixin responsive-text-size ($text-size, $default-breakpoint: 'small') {
  @include text-size($text-size, $default-breakpoint);

  $text-breakpoints-map: text-breakpoints-for($text-size);
  $text-breakpoints-keys: map-keys($text-breakpoints-map);

  @each $breakpoint-name in $text-breakpoints-keys {
    @if $breakpoint-name != $default-breakpoint {
      @include respond-above($breakpoint-name) {
        @include text-size($text-size, $breakpoint-name);
      }
    }
  }
}
~/sass-lib/mixins/_typography.scss

The mixins are not very complex, there are no crazy formulas. The actual implementation is not particularly important but what is important is that it creates a really readable interface to use throughout the rest of the project:

.text--mega {
  @include responsive-text-size('mega');
}

.text--kilo {
  @include responsive-text-size('kilo');
}

.text--hecto {
  @include responsive-text-size('hecto');
}

.text--deca {
  @include responsive-text-size('deca');
}

.text--base {
  @include responsive-text-size('base');
}

.text--deci {
  @include responsive-text-size('deci');
}

.text--centi {
  @include responsive-text-size('centi');
}
~/_typography.scss

I’ve worked on so many codebases littered with different font sizes and they’re hard to manage consistently when building and maintaining a responsive site. This technique sandboxes the problem in an easily understandable way. Let me know what you think about my solution to responsive typography in the comments below or check out an example.

View the demo on SassMeister