Responsive Typography Using Modern CSS

Choosing type sizes for a website can be daunting. Heading and paragraph sizes have drastic consequences over the layout and legibility of a page. Thankfully, we have modular scales to guide us.

A modular scale is a sequence of numbers that relate to one another in a meaningful way. Tim Brown, More Meaningful Typography

Modular scales are used in typography to create visual hierarchy and harmonious proportions. They provide a set of numbers to use as guidelines for font sizes and spacing.

An example modular scale
A modular scale in typography

Implementing a modular scale in the context of the web typically involves manual calculation and hand-coding values in different places in your CSS. Extra care must be taken to maintain balanced proportions on all screen sizes.

We can take advantage of new CSS features to make it easier to design with modular scales in a responsive website. We'll start by using custom properties and calc() to dynamically compute the values for a modular scale. Then we'll change the scale based on viewport size, using media queries and the media() function.

Recommended: PostCSS and cssnext

In order to retain browser compatibility when using modern CSS syntax, I recommend using PostCSS with the cssnext plugin.

A minimal postcss.config.js will look like this:

// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-cssnext'),
  ],
};

See the PostCSS documentation for instructions on how to integrate PostCSS with your preferred build tool (webpack, gulp, etc.).

Implementing a scale

Start by defining your fonts and type scale as custom properties. Use calc() to calculate varying powers of your chosen ratio. We'll use a ratio of 1.414 (the augmented fourth, in musical terms) for the following example.

A few guidelines regarding units:

:root {
  /* Font faces */
  --headerFont: 'Helvetica Neue', sans-serif;
  --bodyFont: 'Georgia', serif;
  --fontColor: hsla(0, 0%, 0%, 0.8);
  --lineHeight: 1.5;
  --baseFontSize: 112.5%;  /* 18px */
  /* Type scale */
  --ratio: 1.414;  /* Augmented fourth */
  /* Each step of the scale is a power
     of --ratio
  */
  --stepUp1: calc(1em * var(--ratio));
  --stepUp2: calc(var(--stepUp1) *
                  var(--ratio));
  --stepUp3: calc(var(--stepUp2) *
                  var(--ratio));
  --stepDown1: calc(1em /
                    var(--ratio));
}

You can then reference these variables when setting your base fonts.

html {
  font-size: var(--baseFontSize);
  color: var(--fontColor);
  line-height: var(--lineHeight);
  font-family: var(--bodyFont);
}

h1,
h2,
h3 {
  margin: var(--stepUp1) 0 0.5em;
  font-family: var(--headerFont);
  line-height: 1.2;
}

h1 {
  font-size: var(--stepUp3);
}

h2 {
  font-size: var(--stepUp2);
}

h3 {
  font-size: var(--stepUp1);
}

small {
  font-size: var(--stepDown1);
}

We get the following result:

See the Pen Type Scaling using cssnext by Steven Loria (@sloria) on CodePen.

The result looks okay, unless you are viewing this post on your phone…

Problems on mobile

The scale used above works well for wide screen viewing. However, a ratio that's appropriate for desktop or laptop viewing is unlikely to work on smaller screens.

Jason Pamental explains why:

As the screen size shrinks and fewer elements are visible, the relative scale between elements becomes exaggerated. Jason Pamental, A More Modern Scale for Web Typography

Consequently, we have disproportionately large headings on small screen sizes.

Non-responsive scale
A modular scale for large screens will not work for smaller devices.

Making it responsive

On smaller viewports, there are fewer visual elements competing for attention. Therefore, we can reduce the base font size and have less contrast between heading sizes.

For our example, let's use the following values:

  • Screens smaller than 36em: base font size of 100% (16px), ratio of 1.2
  • Screens between 36 em and 48em: base font size of 112.5% (18px), ratio of 1.2
  • Screens larger than 48em: base font size of 112.5% (18px), ratio of 1.414
Different scales on different devices
Use different scales and base font sizes for different viewports.

We'll go over two ways to implement the desired result. First, we'll use media queries. Then we'll use the media() function to make the code more concise.

Using media queries

Since we have two type scale ratios, we'll write two sets of variables: one for small and medium screens and one for large screens. We'll also define two base font sizes.

:root {
   /* Base sizes */
  --baseFontSizeSm: 100%; /* 16px */
  --baseFontSizeMd: 112.5%; /* 18px */
  /* Type scale on smaller screens */
  --ratioSm: 1.2;  /* Minor third */
  --stepUp1Sm: calc(1em * var(--ratioSm));
  --stepUp2Sm: calc(var(--stepUp1Sm) *
                    var(--ratioSm));
  --stepUp3Sm: calc(var(--stepUp2Sm) *
                    var(--ratioSm));
  --stepDown1Sm: calc(1em /
                      var(--ratioSm));
  /* Type scale on larger screens */
  --ratioLg: 1.414;  /* Augmented fourth */
  --stepUp1Lg: calc(1em * var(--ratioLg));
  --stepUp2Lg: calc(var(--stepUp1Lg) *
                    var(--ratioLg));
  --stepUp3Lg: calc(var(--stepUp2Lg) *
                    var(--ratioLg));
  --stepDown1Lg: calc(1em /
                      var(--ratioLg));
}

Next, we'll write the two break points to use in our media queries.

Use em units for media queries because they behave the most consistently across browsers. See PX, EM or REM Media Queries? for a comparison of the units.

We'll define our break points as custom media (another cssnext feature) so that we can refer to them by name.

@custom-media --break-md (width >= 36em);
@custom-media --break-lg (width >= 48em);

The base font size should increase at the "medium" break point.

html {
  font-size: var(--baseFontSizeSm);
  color: var(--fontColor);
  line-height: var(--lineHeight);
  font-family: var(--bodyFont);

  @media (--break-md) {
    font-size: var(--baseFontSizeMd);
  }
}

The ratio should increase at the "large" break point, so we refer to the large scale steps in our media queries.

h1 {
  font-size: var(--stepUp3Sm);
  @media (--break-lg) {
    font-size: var(--stepUp3Lg);
  }
}

h2 {
  font-size: var(--stepUp2Sm);
  @media (--break-lg) {
    font-size: var(--stepUp2Lg);
  }
}

h3 {
  font-size: var(--stepUp1Sm);
  @media (--break-lg) {
    font-size: var(--stepUp1Lg);
  }
}

small {
  font-size: var(--stepDown1Sm);
  @media (--break-lg) {
    font-size: var(--stepDown1Lg);
  }
}

See the Pen Responsive modular scale using cssnext by Steven Loria (@sloria) on CodePen.

The font sizes properly adjust to the viewport size. Much better!

This approach is certainly more convenient than hand-calculating values for our scales, but it still requires at least one media query every time we refer to a step in the scale.

We can make the code even more concise with the media() function, which we'll dig into next.

Using the media() function

Rather than defining separate sets of variables for each modular scale, we can use the media() function (spec) to assign responsive values to our scale properties.

This feature requires the postcss-media-fn plugin in addition to cssnext. Install it with npm or yarn and add it to your postcss.config.js.

npm install postcss-media-fn
// postcss.config.js
module.exports = {
  plugins: [
    require('postcss-cssnext'),
    require('postcss-media-fn'),
  ],
};

Use the media() function when defining your type scale variables. Each step of the scale combines the type scale calculations for each break point.

A step in our scale will look like this:

--stepUp1: media(
  calc(1em * var(--ratioSm)),
  (min-width: 48em) calc(1em * var(--ratioLg))
);

Let's break it down.

  • The default size for the first step of the scale is 1em × 1.2 (our smaller ratio).
  • Beyond 48em (our "large" break point), the first step is 1em × 1.414 (our larger ratio).

Here is the full scale using media():

:root {
  /* Break points */
  --minMd: 36em;
  --minLg: 48em;

  /* Ratio on smaller screens */
  --ratioSm: 1.2; /* Minor third */

  /* Ratio on larger screens */
  --ratioLg: 1.414; /* Augmented fourth */

  /* Type scale */
  --stepUp1: media(
    calc(1em * var(--ratioSm)),
    (min-width: var(--minLg)) calc(1em * var(--ratioLg))
  );
  --stepUp2: media(
    calc(1em * var(--ratioSm) * var(--ratioSm)),
    (min-width: var(--minLg)) calc(1em * var(--ratioLg) * var(--ratioLg))
  );
  --stepUp3: media(
    calc(1em * var(--ratioSm) * var(--ratioSm) * var(--ratioSm)),
    (min-width: var(--minLg)) calc(1em * var(--ratioLg) * var(--ratioLg) * var(--ratioLg))
  );
  --stepDown1: media(
    calc(1em / var(--ratioSm)),
    (min-width: var(--minLg)) calc(1em / var(--ratioLg))
  );
}

With this setup, we no longer need to include media queries when referring to our scale.

h1 {
  font-size: var(--stepUp3);
}

h2 {
  font-size: var(--stepUp2);
}

h3 {
  font-size: var(--stepUp1);
}

small {
  font-size: var(--stepDown1);
}

This approach requires a little more setup, but it results in tidier code when you use your scale in many places.

In summary…

Follow these guidelines when designing with modular type scales on the web:

  • Use custom properties and calc() to dynamically scale your font sizes according to a modular scale.
  • Use percent units for base font sizes. Use em for sizing text and media queries.
  • Use different scales for different viewport sizes. The smaller the viewport, the smaller the type scale ratio can be.
  • Use media queries or the media() function to change the ratio and base font size based on the viewport.

Related links

Please send comments by email. I welcome your feedback, advice, and criticism.