← back to articles

Everything I know about responsive web typography with CSS — Zell Liew

Save article ToRead Archive Delete · Log out

14 min read · View original · zell-weekeat.com

Responsive typography is a tough nut to crack. This was the best method I could come up with when I first started creating responsive websites:

p {
  font-size: 16px;
}

@media (min-width: 800px) {
  p {
    font-size: 18px;
  }
}

/* Repeat for h1 - h6 and other type groups */

I’ve learned a lot more about typography since then and picked up best practices like using relative units, vertical rhythms and meaningful typography scales.

These new practices were wonderful. They made my websites look more pleasing to the eye. Implementing them, however, was a horrible experience.

I had to write complex code and I found myself struggling to create responsive websites under tremendous time pressure.

Now, after months of hacking, I’ve finally created a solution that I’m happy to share with you. It’s called Typi.

Typi is great because it allows me to use the practices I’ve learned, and at the same time solves most of the problems I’ve encountered in 3 simple steps. Let me explain these three steps by walking you through the practices I use when working with responsive typography.

Practice 1: Increase font-size and line-height of your body copy as screen sizes increase.

Reading on the mobile vs the desktop are two completely different experiences. You undoubtedly hold your device closer to you when you read on the mobile since the screen is smaller.

Your desktop screen will be further away from you compared to the mobile. Hence, the same font-size on the desktop looks slightly smaller due to the further distance.

To increase readability and compensate for the loss in size due to distance, we increase the font size.

I first got to know of this practice through the responsive typography basics post on ia.net. I highly suggest checking the post out if you’re unfamiliar with what I’m talking about.

An implementation of this practice in Sass can be this:

html {
  font-size: 16px;

  @media (min-width: 800px) {
    font-size: 18px;
  }

  @media (min-width: 1200px) {
    font-size: 20px;
  }
}

Note: as we increase font sizes, we might also need to increase the line height to allow more breathing room between lines of text. With Sass, this can look like:

html {
  font-size: 16px;
  line-height: 1.3;

  @media (min-width: 800px) {
    font-size: 18px;
  }

  @media (min-width: 1200px) {
    font-size: 20px;
    line-height: 1.4;
  }
}

Practice 2: Use a modular scale for your typography

It’s difficult to pick the font-size of your typographic elements (<h1> to <h6>), especially if you’re trying to pull them out of thin air. A modular scale, also called typography scale, is a tool you can use to help you pick good typography sizes that flow well with the rest of your page.

It is a sequence of numbers related to each other through a ratio (a number). It can be created by multiplying (or dividing) the font-size of your body copy with the ratio. The resultant number is then multiplied (or divided) again with the ratio.

An implementation of modular scale at work with the above scale can be this:

html { font-size: 16px }
h1 { font-size: 50px }
h2 { font-size: 37px }
h3 { font-size: 28px }
// ...

Of course, it’s not going to be so simple. If you remember the first practice we discussed earlier in this article, you’ll notice that the body font-size should change as your screen width changes.

This becomes a problem when you have to change the typography sizes of all your elements at every breakpoint to ensure the scale stays consistent.

html {
  font-size: 16px;
  line-height: 1.3;
  @media (min-width: 800px) {
    font-size: 18px;
  }

  @media (min-width: 1200px) {
    font-size: 20px;
    line-height: 1.4;
  }
}

h1 {
  font-size: 50px;
  @media (min-width: 800px) {
    font-size: 56px;
  }
  @media (min-width: 1200px) {
    font-size: 63px;
  }
}

h2 {
  font-size: 37px;
  @media (min-width: 800px) {
    font-size: 42px;
  }
  @media (min-width: 1200px) {
    font-size: 47px;
  }
}

h2 {
  font-size: 28px;
  @media (min-width: 800px) {
    font-size: 32px;
  }
  @media (min-width: 1200px) {
    font-size: 35px;
  }
}
// ...

Ugh.

The solution? Check out the next practice.

Note: If you need help with choosing your starting font-size and ratio for your modular scale, I suggest reading this article on meaningful typography by Tim Brown.

Practice 3: Use relative typography units

Relative units in CSS are percentages (%), viewport units (vh, vw, vmin, vmax) , the em unit (em) and the rem unit (rem). The ones commonly used to size typography are em and rem.

You can use both em and rem in the same manner to solve the problem we encountered in practice 2. To convert pixels into em, we take the target font-size and divide it against the base-font size.

Here’s how it would look:

html {
  font-size: 16px;
  @media (min-width: 800px) {
    font-size: 18px;
  }
  @media (min-width: 1200px) {
    font-size: 20px;
  }
}

h1 { font-size: 3.125em; } // 50 ÷ 16 = 3.125
h2 { font-size: 2.3125em;} // 37 ÷ 16 = 2.3125
h3 { font-size: 1.75em; } // 28 ÷ 16 = 1.75
// ...

// Note: These are approximate values.
// The actual values derived from modularscale.com are 3.129em, 2.3353em and 1.769em respectively.

Much better now!

There are a few more problems. Notice how <h1> becomes approximately 63px as the screen width increases to 1200px.

63px is pretty large. Reading the <h1> text starts to get uncomfortable already. A better decision might be to tone it down to 47px instead (size of <h2>).

When this happens, you might want to decrease the size of the <h2> element since it’s a good practice to emphasize the <h1> element. Sometimes, you might also need to change the line-height as well.

The code then becomes…

html {
  font-size: 16px;
  @media (min-width: 800px) {
    font-size: 18px;
  }
  @media (min-width: 1200px) {
    font-size: 20px;
  }
}

h1 {
  font-size: 3.129em;
  line-height: 1.2;

  @media (min-width: 1200px) {
    font-size: 2.3353em;   
    line-height: 1.3;
  }
}

h2 {
  font-size: 2.3353em;
  @media (min-width: 1200px) {
    font-size: 1.769em;   
  }
}

h3 {
  font-size: 1.769em;
  @media (min-width: 1200px) {
    font-size: 1.33em;
  }
}

// ...

Ugh. Back to square one :(

This is where Typi comes in. Let’s take a break from the practices and see how Typi can help you.

Using Typi

Typi is a Sass library that allows you to set up font-size and line-height properties of all your typographic elements in separate Sass maps. These maps can then be used to output code we see in the above situation. Here’s how it works.

First, you need to setup a $typi map. It looks like this:

$typi: (
  null: 16px,
  small: 18px,
  large: 20px
);

null, small and large are breakpoints.

Typi automatically looks for a $breakpoints map to create your media queries (which means it can integrate perfectly with mappy-breakpoints, a library I created to help with media queries).

$breakpoints: (
  small: 800px,
  large: 1200px
);

Once the $typi map is set up, we call the typi-base() mixin within the html selector.

html {
  @include typi-base();  
}

This typi-base() mixin creates the same styles we wrote for the <html> tag in Practice 2. The only difference is that the font-sizes are stated in percentages.

html {
  font-size: 100%; /* This means 16px */
}

@media all and (min-width: 800px) {
  html {
    font-size: 112.5%; /* This means 18px */
  }
}

@media all and (min-width: 1200px) {
  html {
    font-size: 125%; /* This means 20px */
  }
}

We also mentioned that there might be a need to change line-height values as the font-size changes. You can change line-height values easily in Typi by providing a second line-height value to each breakpoint that requires it:

$typi: (
  null: (16px, 1.3), // Sets line-height to 1.3
  small: 18px,
  large: (20px, 1.4) // Sets line-height to 1.4
);

The resultant CSS from our updated $typi map is:

html {
  font-size: 100%; /* This means 16px */
  line-height: 1.3;
}

@media all and (min-width: 800px) {
  html {
    font-size: 112.5%; /* This means 18px */
  }
}

@media all and (min-width: 1200px) {
  html {
    font-size: 125%; /* This means 20px */
    line-height: 1.4;
  }
}

After creating the $typi map, we can create other font-maps using the same format. Here’s an example:

$h1-map: (
  null: (3.129em, 1.2),
  large: (2.3353em, 1.3)
  );

$h2-map: (
  null: 2.3353em,
  large: 1.769em
  );

$h3-map: (
  null: 1.769em,
  large: 1.333em
  );
// ...

Then, we call each of these font-maps using the typi mixin:

h1 { @include typi($h1-map) }
h2 { @include typi($h2-map) }
h3 { @include typi($h3-map) }
// ...

The resultant CSS would be:

h1 {
  font-size: 3.129em;
  line-height: 1.2;
}

@media (min-width: 1200px) {
  h1 {
    font-size: 2.3353em;
    line-height: 1.3;
  }
}

h2 {
  font-size: 2.3353em;
}

@media (min-width: 1200px) {
  h2 {
    font-size: 1.769em;
  }
}

h3 {
  font-size: 1.769em;
}

@media (min-width: 1200px) {
  h3 {
    font-size: 1.333em;
  }
}

Pretty neat huh? You’ll have to download Typi to play with it. (It’s not available on Sassmeister or Codepen yet)

PROTIP: You can use the modular scale Sass mixin if you don’t want to write exact em values (like 1.769em) across different font maps.

To do so, you have to download the library and import it into your Sass file. Then, change the font maps such that it uses the ms() function.

$h1-map: (
  null: (ms(4) 1.2),
  large: (ms(3), 1.3)
  );

$h2-map: (
  null: ms(3),
  large: ms(2)
  );

$h3-map: (
  null: ms(2),
  large: ms(1)
  );
// ...

So, in a nutshell, Typi makes responsive typography easier by helping you write font-size and line-height properties at different breakpoints`.

Now that I’m done introducing you to Typi, let’s move on and talk about the final two practices (and some problems that I have yet to find a solution for).

Practice 4: Apply vertical rhythms

Vertical Rhythms is a concept from print design (I think), where we keep vertical spaces between elements on a page consistent (and relative) to each other. The idea is similar to using a typography scale – to allow elements on your page to flow well.

In practice, we often use the line-height property of the body copy as the base for a consistent vertical rhythm. Let’s say the body copy of your website has line-height of 25px. You’ll do two things:

  1. Set the vertical white space between elements to a multiple of 25px
  2. Set the line-height of all text elements to a multiple of 25px

This is how it might look like in CSS (Note: This hasn’t taken the three practices I mentioned above into account yet)

html {
  font-size: 18px;
  line-height: 25px;
}

// Resets margins
body, h1, p {
  margin: 0;
}

h1 {
  font-size: 63px;
  line-height: 75px;
  margin: 25px 0;
}

p + p {
  margin-top: 25px;
}

Looks pretty good! Let’s take it a step further by changing the code above into relative units. While doing so, you’ll encounter the great em vs rem debate.

Em vs Rem

Let’s try converting the code first into ems, then rems. By the way, the best practices states that line-height values should be unitless.

html {
  font-size: 1.125em;
  line-height: 1.4; // This is 25.2px to be accurate
}

// Resets margins
body, h1, p {
  margin: 0;
}

h1 {
  // font size is 63.147px to be more precise
  font-size: 3.5082em; // 63.147 ÷ 18 = 3.5082em
  line-height: 1.1972; // 75.6 ÷ 63.147 =  1.1972
  margin: 0.3991em 0; // 25.2 ÷ 63.147 = 0.3991
}

p + p {
  margin-top: 1.4em;
}

Pay special attention to how we converted the margin property on the <h1> element into ems.

Notice how we used 63.147px as the base for the division? This must be done because sizes calculated with ems are relative to it’s current font-size. It often causes confusion and involves a lot of complex math.

Now, here’s the kicker. Even though we tried to be as accurate as we can be, browsers don’t seem to cooperate with us. You’ll notice that our vertical rhythms start getting screwy.

Two problems contributed to this screwy behavior.

First, we’re not 100% precise and accurate with our math. We could get more precise (like 10 decimal places), but that would make our code ugly as hell…

Second, different browsers handle subpixel rounding issues differently. This means we’ll never be able to get pixel-perfect rhythms no matter how hard we try.

Well, I don’t want to harp on subpixel rounding because there’s nothing much we can do. Let’s take a look at how the rem unit handles this complex math instead, shall we?

html {
  font-size: 1.125rem;
  line-height: 1.4; // This is 25.2px to be accurate
}

// Resets margins
body, h1, p {
  margin: 0;
}

h1 {
  font-size: 3.5082rem; // 63.147 ÷ 18 = 3.5082
  line-height: 1.1972; // 75.6 ÷ 63.147 = 1.1972
  margin: 1.4rem 0; // 25.2 ÷ 18 = 1.4
}

p + p {
  margin-top: 1.4rem;
}

Notice how we used 1.4rem on the margin property instead of 0.3991em? The rem unit makes calculations with vertical rhythms much simpler.

This doesn’t mean you should switch blindly to the rem unit though. Rems and em units are both useful, and they should be used for different purposes. I’ll write about this topic another day. For now, let’s come back to vertical rhythms.

Now that we’ve converted our vertical rhythms into relative units, let’s take a look at how it fares when we combine it with practice one (font-sizes and line-heights should change when screen sizes change).

We’re going to keep this example as simple as possible by using only one media-query. We’re also going to use the rem unit.

html {
  font-size: 1.125em;
  line-height: 1.4;

  @media (min-width: 1200px) {
    font-size: 1.25em; // this is 20px
    // Slight change in line heights at 1200px
    line-height: 1.45 // this is 29px
  }
}

// Resets margins
body, h1, p {
  margin: 0;
}

h1 {
  font-size: 3.5082em;
  line-height: 1.1972;
  margin: 1.45rem 0;

  @media (min-width: 1200px) {
    // font-size is now 70.164px
    line-height: 1.24; // 29px * 3 ÷ 70.164 = 1.24
    margin: 1.45rem 0;
  }
}

p + p {
  margin-top: 1.4em;
  @media (min-width: 1200px) {
    margin-top: 1.45em
  }
}

Ugh. We might have to add 20,000 media queries to change margin and line-height of all our elements with just this one change in the line-height property on <html>. We haven’t even talked about padding or border properties yet!

(╯°□°)╯︵ ┻━┻

So, here’s what I realized. It’s impossible to apply perfect responsive vertical rhythms across different browsers. At least not with the current technology.

What we can do instead is:

  1. We can make do with line-height properties of major typographic elements by eyeballing and using Typi.
  2. Try not to change the line-height property of your body copy if you can. Things will become easier when [CSS variables]((http://caniuse.com/#search=css%20var) are finally supported in all major browsers.

Practice 5: Keep text measures between 45 – 75 characters

Oh, this one is easy. Just remember this: one character is approximately 0.5em. A text measure between 45 – 75 characters means the width of your text element must be between 22.5em and 37.5em.

From experience, I’m mostly concerned about text overflowing 75 characters. If your text goes below 45 characters, maybe it’s time to have a change of font sizes.

article {
  max-width: 30em;
  /* Anywhere between 22.5em to 37.5em. Use your discretion */
}

Wrapping Up

Responsive typography is hard. There are still no perfect answers we can rely on, but let’s try to make do for now.

By the way, here’s the link to Typi again if you want to play with it.

Get my best content on frontend development and design delivered to you each week for free

I'm so thrilled to see that you wanna hear more from me! Go check your email now :)