Phillip Luther’s Frontend Developer Experience Blog

Using CSS Custom Properties and Calc To Build a Type Scale

Published on

I love CSS custom properties. I love calc. We can use CSS custom properties and calc to build a fluidly responsive type scale in vanilla CSS. Let's do so!

Laura Chouette via Unsplash

I love building systems. Type scales are systems. I love type scales! I love CSS, too, and have been on an anti-pre-/post-processor kick for the last few years. Media queries? Love ‘em. Also, t-shirt sizes.

Let’s merge these loves into one giant super love and build ourselves a type scale in vanilla CSS! As mentioned, we’ll heavily use CSS custom properties and calc(). We’ll be using some media queries, too.

At the end of this, we’ll have a handy little set of utilities we can port around and sanely size typography in any web app, regardless of UI framework.

Covering the Bases

If you’re familiar with type scales, CSS custom properties, and CSS’s calc function, feel free to skip ahead. Otherwise, let’s take a quick look at each of these things to get a sense of what we’re building and how we’re making it.

Type Scales

Type scales give typography rhythm and flow, an essential piece of any well-designed app’s je ne sais quoi.

More specifically, a type scale is a system of rules governing the relative size of text. It determines how big a headline should be compared to paragraph text size or a subheading compared to fine print. A type scale provides a sane way of maintaining font size consistency.

All the major design systems use a type scale. Here’s what Material Design says about theirs; and Atlassian about theirs; and Shopify about theirs, Polaris.

CSS Custom Properties

Note that I refuse to call them CSS variables. “CSS variables” is pre-/post-processor lingo. We’re using stock CSS to build our type scale, so we’ll use the term “CSS custom properties.”

What’s a CSS custom property? Well … it’s a CSS variable. You can define a property and use it in multiple places.

Want your buttons and links to be the same color? CSS custom properties to the rescue!

:root {
  --primary-cta-color: #0f0; /* yikes! */
}

button {
  background-color: var(--primary-cta-color);
}

a {
  color: var(--primary-cta-color);
}

Here’s a deeper dive into CSS custom properties, including explanations of that :root and var() cost of doing business.

CSS calc()

If you’ve been sleeping on calc, you’re in for a treat. CSS’s fantastic calc function lets you do calculations in your CSS properties. It’s most triumphant for mixing relative and fixed values.

A classic example involves percentages and pixels, like if you’re creating columns and want a fixed gutter between ‘em.

.column {
  width: calc(50% - 24px);
}

MDN’s got you covered, as always, for further reading on calc. They’ve even got a subsection on using calc() with CSS custom properties. Using calc with CSS custom props is precisely what we’re about to do as we build out our type scale.

Speaking of …

Our Type Scale

We’re gonna keep things moderately simple. Our type scale will have 5 sizes inspired by a retail rack of t-shirts: extra small, small, medium, large, and extra large. It’ll use a fixed ratio of 1.333, meaning each size will be 1.333 times bigger than the previous one. We’ll also create a set of utility classes for applying our type scale.

Onward!

Creating the Scale and Base Font Size

First, let’s create a few custom properties to establish the scale and a base font size. We want these properties to be global, so we’re throwing them in the :root CSS scope.

:root {
  --font-size-base: 1rem;
  --font-scaling: 1.333;
}

Easy enough. Let’s chat about that --font-size-base, though.

The Base Font Size

Our type scale governs the relative size of our fonts; it’s unconcerned by pixel (or other fixed) values. Setting our base font size to 1rem — effectively saying, “Use the user’s default font size” — merely gives us a starting point to apply the scale.

Scaling Properties

Let’s create custom properties for each of our font sizes. We’ll use the small size as our baseline, then scale up or down by our 1.333 ratio proportionally. Here’s where calc shines.

:root {
  ...

  --font-size-xs: calc(var(--font-size-sm) / var(--font-scaling));
  --font-size-sm: var(--font-size-base);
  --font-size-md: calc(var(--font-size-sm) * var(--font-scaling));
  --font-size-lg: calc(var(--font-size-md) * var(--font-scaling));
  --font-size-xl: calc(var(--font-size-lg) * var(--font-scaling));
}

Slick, right?

--font-size-xs is 1.333 times smaller than --font-size-sm, --font-size-md is 1.333 times bigger than --font-size-sm, and so on, up to our largest size.

When those calcs get calc’d, the sizes basically look like this:

:root {
  /* post-calc demonstration purposes only, with convenient rounding applied */
  --font-size-xs: 0.75rem;
  --font-size-sm: 1rem;
  --font-size-md: 1.333rem;
  --font-size-lg: 1.777rem;
  --font-size-xl: 2.369rem;
}

If we don’t like the look of our scale, we can play around with --font-size-base and --font-scaling, but we won’t have to update anything else because our sizes are relative. If we wanted to extend the scale, we could create a 2xl size similarly, multiplying --font-size-xl by --font-scaling.

Responsive Out-of-the-Box

Cooler still, we get responsive behavior for free with our type scale since it’s not pegged to fixed values. Our 1rem base unit will pick up any font size adjustments made elsewhere in our CSS.

We could adjust our scale based on screen size by updating the custom properties.

Check this out:

:root {
  ...
  --font-scaling: 1.333;
  ...
}

@media screen and (min-width: 800px) {
  :root {
    --font-scaling: 1.414;
  }
}

By default, our fonts grow at a ratio of 1.333. --font-size-md calcs to 1.333rem and --font-size-xl calcs to 2.369rem. At tablet-ish sizes, our font scaling gets wider: -md becomes 1.414rem and -xl becomes 2.827rem. And we only changed a single property!

I wouldn’t recommend futzing with the --font-size-base. I’ve found it much more manageable to leave its value at 1rem so it inherits the default font sizes set on the document. That’s merely my two pennies; get creative and use what works for you.

Using Our Sizes

Our type scale is all well and good, though it currently doesn’t affect anything. How do we actually use it? Let’s create some utility classes that consume our custom properties.

.text-xs {
  font-size: var(--font-size-xs);
}

.text-sm {
  font-size: var(--font-size-sm);
}

.text-md {
  font-size: var(--font-size-md);
}

.text-lg {
  font-size: var(--font-size-lg);
}

.text-xl {
  font-size: var(--font-size-xl);
}

Those classes might look familiar if you like popular CSS utility class libraries. Now, though, we can slap those on any HTML element and apply our scaling.

<p class="text-xs">I'm very small text.</p>
<p class="text-xl">Conversely, I'm huge.</p>

For any non-fans of utility classes or popular CSS utility class libraries, we can use our sizing properties directly in our other CSS since they’re global (:root { … }).

.banner-headline {
  font-size: var(--font-size-xl);
}

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

The Whole Shebang

Here’s our full type scale in CSS, including the utility classes and a little extra flavor I added for responsive font sizes to demonstrate how our type scale “just works.”

:root {
  --font-size-base: 1rem;
  --font-scaling: 1.333;

  --font-size-xs: calc(var(--font-size-sm) / var(--font-scaling));
  --font-size-sm: var(--font-size-base);
  --font-size-md: calc(var(--font-size-sm) * var(--font-scaling));
  --font-size-lg: calc(var(--font-size-md) * var(--font-scaling));
  --font-size-xl: calc(var(--font-size-lg) * var(--font-scaling));
}

.text-xs {
  font-size: var(--font-size-xs);
}

.text-sm {
  font-size: var(--font-size-sm);
}

.text-md {
  font-size: var(--font-size-md);
}

.text-lg {
  font-size: var(--font-size-lg);
}

.text-xl {
  font-size: var(--font-size-xl);
}

@media (min-width: 600px) {
  html {
    font-size: 1.1em;
  }
}

@media (min-width: 1100px) {
  :root {
    --font-scaling: 1.414;
  }

  html {
    font-size: 1.25em;
  }
}

Pretty lean for something so powerful. Here’s a CSS file and demo of the above in action, too.

Bless You, CSS Custom Properties and calc()

While our type scale is intentionally simple, we could expand the system to handle complex cases. I usually include something like this in all my projects to ensure the typography maintains consistency and rhythm, regardless of scope.

It’s much better than needing to memorize values. It’s also an absolute joy to tweak since it has so few moving parts once the initial rules are established.

Sometimes, I play around with where the base font size sits, calling medium the base. Sometimes, I might even have a .text-base utility class, where small is even smaller and medium is slightly bigger. The general approach of using CSS custom props and calc stays the same. It’s a flexible, customizable approach. That’s why I love it.

Play around with it! You might love it, too.

Also, I’m still refusing to call them CSS variables.