Modular Scale Typography with CSS Variables and Sass

Typography nerds and web designers tend to agree that choosing font size values based on a modular scale is a way of adding intent and meaning to type design. Rather than setting body and heading sizes arbitrarily—16px, 18px, 20px, 30px, all set!—using a scale allows the sizes to, ahem, harmonize.

With help from modern CSS tools, we don't even have to do any math. Here, I'll look at how to do this simply with CSS variables, with the Sass pre-processor, and by pairing the two to support older browsers.

First we need a ratio. The Modular Scale website is a terrific resource to get a visual idea of what different type scales look like. Let's say we like the perfect fourth (3 to 4, or the ratio number 1.333). Let's set that up with CSS variables.

:root {
  --font-size: 1rem; /* 16px */
  --ratio: 1.333;
}

Using the relative 1rem as a baseline means we can set our document base with font-size: 100%; in our :root or html element and use media queries and even a fluid typography calculation to change all font sizes, together.

Now we'll set our type sizes, moving up the scale. Here's the fun part where we don't have to do any math. You can use whatever naming conventions you want for this: some people like f1, f2, etc. as a shorthand for "font," others might want to use the familiar h1, h2, etc. I'll use h1 and set up four sizes. We'll add this to the :root pseudo-class:

  --h4: calc(var(--font-size) * var(--ratio));
  --h3: calc(var(--h4) * var(--ratio));
  --h2: calc(var(--h3) * var(--ratio));
  --h1: calc(var(--h2) * var(--ratio));

See what we did there? Each heading size goes up one mark on the scale. If you'd like to jump two levels and need more sizes, start down at h6 or h8 or change the math to add more ratio multipliers.

So now we have:

:root {
  --font-size: 100%; /* 16px */
  --ratio: 1.333;

/* Calculate values */
  --h4: calc(var(--font-size) * var(--ratio));
  --h3: calc(var(--h4) * var(--ratio));
  --h2: calc(var(--h3) * var(--ratio));
  --h1: calc(var(--h2) * var(--ratio));
}

And we can set our text sizes:

p {
  font-size: var(--font-size);
}

h4 {
  font-size: var(--h4);
}

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

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

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

By picking just two numbers, font size and ratio, we've set a beautiful modular scale for all the type on our website.

The modular scale in Sass

Do we need to use CSS variables? Can't we do this in Sass?

We sure can:

$font-size: 100%;
$ratio: 1.333;

/* Calculate values */
$h4: $font-size * $ratio;
$h3: $h4 * $ratio;
$h2: $h3 * $ratio;
$h1: $h2 * $ratio;
p {
  font-size: $font-size;
}

h4 {
  font-size: $h4;
}

h3 {
  font-size: $h3;
}

h2 {
  font-size: $h2;
}

h1 {
  font-size: $h1;
}

For a single scale, the Sass method is actually superior. It uses fewer lines of code, will render a bit more quickly in a browser (CSS variables are slower performers than hard values), and is compatible everywhere because it just uses font-size: CSS variables won't work in any version of Internet Explorer and are only up to 87% browser compatibility overall, according to Can I Use. (This is up from 77% last time I checked, and up to 96% of U.S. mobile users, so compatibility may be a minor issue unless you specifically need to support Internet Explorer and Opera Mini.)

But as Zell Liew points out in his post on Responsive Modular Scale, it's hard to make a single ratio work between mobile/smaller and desktop/larger screens. Smaller screens need more uniform text and larger screens need a more dynamic ratio for effective design. He offers several ways to go about this: use a smaller ratio, add a second base number to your scale, add a second ratio, or change the ratio at different breakpoints.

I'll add one more option, which is: use the same ratio, but change the steps between sizes at different devices. As Drew Powers notes, Medium.com uses a clever method here: Step 1 and 3 for subheadings and headings on mobile, Step 2 and 4 on desktop.

If all we want to do is change the steps, we can do it cleanly in Sass with a media query.

First let's add a few more sizes to have some range.

$font-size: 100%;
$ratio: 1.333;

/* Calculate values */
$h6: $font-size * $ratio;
$h5: $h6 * $ratio;
$h4: $h5 * ratio;
$h3: $h4 * $ratio;
$h2: $h3 * $ratio;
$h1: $h2 * $ratio;
p {
  font-size: $font-size;
}

/* note the smaller sizes for mobile */
h4 {
  font-size: $h6;
}

h3 {
  font-size: $h5;
}

h2 {
  font-size: $h4;
}

h1 {
  font-size: $h3;
}

/* Bring everything up two steps for larger screens */
@media (min-width: 40em) {
  h4 {
  font-size: $h4;
  }

  h3 {
  font-size: $h3;
  }

  h2 {
  font-size: $h2;
  }

  h1 {
  font-size: $h1;
  }
}

But personally, I find switching ratios between smaller and larger screens (or between vertical and horizontal displays) is the most effective, and saves us the bother of keeping track of multiple heading sizes and which one goes where.

CSS variables and multiple ratios

This is where CSS variables are magic. Going back to our original example, we'll add a second ratio variable, --ratio-alt. Everything else stays the same.

:root {
  --font-size: 100%;
  --ratio: 1.333;
  --ratio-alt: 1.68; /* the golden ratio */

/* Calculate values */
  --h4: calc(var(--font-size) * var(--ratio));
  --h3: calc(var(--h4) * var(--ratio));
  --h2: calc(var(--h3) * var(--ratio));
  --h1: calc(var(--h2) * var(--ratio));
}

p {
  font-size: var(--font-size);
}

h4 {
  font-size: var(--h4);
}

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

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

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

All we have to do is add a quick media query.

@media (min-width: 40em) {
  :root {
    --ratio: --ratio-alt;
  }
}

This is where CSS variables beat Sass variables: because the Sass values have already been processed and printed to the stylesheet, they can't be dynamically changed with a media query. CSS variables, being real variables and not processed ones, can change the entire document with a single declaration. This is an incredible super-power.

CSS variables and Sass together

But now we're back to CSS variables and browser compatibility, if we still want to support Internet Explorer. (Sigh.) Luckily, we can pair Sass variables with CSS ones and write a few quick fallbacks without—still—having to do any math or anything particularly tricky.

/* Start with Sass to set all numbers for the stylesheet */

$font-size: 100%;
$ratio: 1.333;
$ratio-alt: 1.68; 

/* that's all our numbers! */

/* Do math: */
$h4: $font-size * $ratio;
$h3: $h4 * $ratio;
$h2: $h3 * $ratio;
$h1: $h2 * $ratio;

/* Set the alternative ratio */
$h4-alt: $font-size * $ratio-alt;
$h3-alt: $h4-alt * $ratio-alt;
$h2-alt: $h3-alt * $ratio-alt;
$h1-alt: $h2-alt * $ratio-alt;

/* use interpolated Sass variables to set the CSS variables */
:root {
  --font-size: #{$font-size};
  --ratio: #{$ratio};
  --ratio-large: #{$ratio-large};

/* More math - has to be actual variables for the media query to work */
  --h4: calc(var(--font-size) * var(--ratio));
  --h3: calc(var(--h4) * var(--ratio));
  --h2: calc(var(--h3) * var(--ratio));
  --h1: calc(var(--h2) * var(--ratio));
}

p {
  font-size: $font-size; /* IE fallback */
  font-size: var(--font-size);
}

h4 {
  font-size: $h4; /* you get the idea */
  font-size: var(--h4);
}

/* switch to alt ratio */

@media (min-width: 40em) {
  
  /* We're going to override this with the fallbacks */
  :root {
	--ratio: var(--ratio-alt); 
  }
  
  h4 {
  font-size: $h4; 
  /* etc. */
}

A note about this: you could override the fallbacks in the media query by setting up the CSS variables again, like this:

@media (min-width: 40em) {
  
  /* We're going to override this with the fallbacks */
  :root {
	--ratio: var(--ratio-alt); 
  }
  
  h4 {
  font-size: $h4; 
  font-size: var(--h4);
  }
}

But this is silly: it's just extra code, the Sass number will be the same, and we don't need them without the fallbacks anyway. Using the fallbacks in the media query, you also don't need the new :root ratio, either, but I'm leaving it in as a way to future-proof the stylesheet.

Remember, CSS variables are up to 96% coverage in U.S. mobile devices: another way to do this would be to set the media query to cover those devices and drop the fallbacks within the query, like this:

$ratio: 1.68; /* desktop ratio */
$ratio-alt: 1.333; /* mobile ratio */

/* ... math goes here ... */

:root {
  --font-size: #{$font-size};
  --ratio: #{$ratio};
  --ratio-large: #{$ratio-large};

/* use fallbacks for desktop */
p {
  font-size: $font-size; /* IE fallback */
  font-size: var(--font-size);
}

/* use max-width instead of min to target small screens */
@media (max-width: 40em) {
  :root {
	--ratio: var(--ratio-alt); 
  }
  
  /* No fallbacks necessary */  
}

Wrapping up

You're now ready to bring meaningful, dynamic, browser-proofed typography to your websites with the power of CSS and Sass variables and the modular scale. Happy typing!

Mike Riethmuller has a great article in Smashing Magazine about CSS custom properties (variables) vs. Sass and why you might not want to use my approach here, and also discussed it on his own Codepen page.

Give this a try on Codepen.

See the Pen 2 Modular Scale Ratios with CSS Variables with Sass Fallback by David Greenwald (@davidegreenwald) on CodePen.

And read more:

Learn Sass: Just the Easy Stuff
Responsive Modular Scale
Responsive Modular Typography Scales in CSS