Skip to content
Courses CSS Nouveau Vanilla Has Never Tasted So Hot Design Tokens from the Ground Up

Design Tokens from the Ground Up

Let’s talk about CSS Custom Properties! #

They’re also called CSS Variables.

Yeah, I know. It’s confusing. 😅 There’s a technical reason for this (isn’t there always?!), but from here on let’s just call them variables because that’s precisely what they are.

They’re (changeable) values you can insert into (almost) every place you would put an actual property value in CSS.

The syntax is very simple. You define variables using names which begin with a double dash. Then you use the var function to reference those variables within style rules.

Globally-scoped variables are typically added to the :root selector. So put it all together and it looks like this:

/* Let's define some variables! */
:root {
  --font-sans: Montserrat, sans-serif;
}

/* Now let's use them! */
body {
  font-family: var(--font-sans);
}

✨ Live Demo ✨ #

Why They’re So Useful #

Sure, you could write a selector like this:

h1 {
  font-size: 3rem;
}

But what if down the line you have another tag where you want to use the same font size as an h1? Perhaps you write this:

figure.is-fancy figcaption {
  font-size: 3rem;
}

But then someone (you?) comes along and decides headings look too big, so h1’s font size gets changed to 2.5rem. Guess what? Now figcaption is stuck with the wrong font size! 😬

There have been build tools in the past to deal with this sort of thing, Sass being one of the most popular. But the awesome news is, we can do this now in pure CSS!

Fun with Variables #

The first step in solving the above would be to define a typographic scale — aka a series of variables for font sizes you can reference anywhere in your code. So instead of the above, maybe you write:

/* In your variables stylesheet: */
:root {
  /* Copied from Open Props: */
  --font-size-00: .5rem;
  --font-size-0: .75rem;
  --font-size-1: 1rem;
  --font-size-2: 1.1rem;
  --font-size-3: 1.25rem;
  --font-size-4: 1.5rem;
  --font-size-5: 2rem;
  --font-size-6: 2.5rem;
  --font-size-7: 3rem;
  --font-size-8: 3.5rem;
}

/* In another stylesheet: */
h1 {
  font-size: var(--font-size-7);
}

/* Later: */
figure.is-fancy figcaption {
  font-size: var(--font-size-7);
}

Aha! We’ve successfully utilized the principle of DRY (Don’t Repeat Yourself) to avoid defining a particular size more than once. By assigning 3rem to --font-size-7, we can now use var(--font-size-7) anywhere to pull in that value. We call a variable like this a design token.

Design tokens can be anything you commonly use throughout your design system (your theme, your layouts and page templates, your components, the works). Design tokens can encompass ideas such as:

Runtime Evaluation For The Win #

Something very important you should know about CSS Variables is that they are evaluated at runtime by the browser. This is in stark contrast to other build systems such as Sass variables or Tailwind theme configs. Because the browser resolves the variables at the time an element is rendered via the DOM, you can inspect them and even modify them directly in your browser’s dev tools.

And one fascinating bonus is that you can use a variable directly inside of a style attribute and can even redefine a variable directly inside style!

<h1 style="--font-size-7: 2rem">  
  Now I'm a smaller 2rem heading!
  
  <span style="font-size: var(--font-size-7)">
    Also 2rem!
  </span>
</h1>

<p style="font-size: var(--font-size-7)">
  But over here, I'm still a 3rem paragraph!
</p>

Or you can target specific elements and redefine their “default” tokens:

/* Base theme stylesheet: */
.page-header {
  --header-background: var(--blue-2);

  background-color: var(--header-background);
}

/* Later in an overrides stylesheet: */
.page-header {
  --header-background: var(--orange-2);
}

It’s almost…it’s almost like…you can build your own styling APIs now right out of vanilla CSS! Whoa! 🤯

✨ Live Demo ✨ #

Your Style Guide Enshrined in Code #

So CSS Variables are pretty rad, and you can use them to craft a set of design tokens you can then use throughout your stylesheets and component libraries. They become extra super-duper useful in the context of designing web components, because unlike many traditional styling techniques, variables pierce shadow roots so components can easily consume site-wide design tokens.

I should back up a step at this point. There’s actually a conceptual issue with the above heading example: it doesn’t necessarily solve the originally stated problem, which is that we want to be able to tweak the heading font size and affect both the actual h1 tag as well as a particular figcaption. Even once we have our typographical scale set up, it’s possible we’ll want to tweak headings specifically, not change the scale. By changing the --font-size-7 design token, we affect everything across the system which references that font size, not just headings per se.

With our base set of tokens in place like --font-size-7 let’s now start to build out a real style guide, a “semantic design system” which we build on top of the base-level. So instead of h1 directly referencing --font-size-7, we’ll employ something more specific.

:root {
  /* A set of absolute font sizes here: */
  --font-size-7: 3rem;
  
  /* Later on, a set of semantic font sizes: */
  --heading-size-lg: var(--font-size-7);
}

/* And in our element and component styles: */

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

figure.is-fancy figcaption {
  font-size: var(--heading-size-lg);
}

Wow! This is starting to look more like a real project. Now we have a heading-specific design token, which itself uses a generic font size token. Guess what? This lets us tweak the heading sizes in a straightforward and deterministic way. If we decide “large headings” are too large, we can simply change --heading-size-lg to var(--font-size-6) or even a totally custom font size if truly required.

Components Tokens as Styling API #

As you saw with the fancy figure example above, we can define and use any number of variables at the component level. Here’s an example of a product listing:

article.is-product {
  --product-heading-size: var(--font-size-5);
  --product-button-color: var(--cyan-8);
  --product-description-font-weight: 600;
}

article.is-product > header hgroup > *:first-child {
  font-size: var(--product-heading-size);
}

article.is-product > p {
  font-weight: var(--product-description-font-weight);
}

article.is-product > footer button {
  background-color: var(--product-button-color);
}
<article class="is-product">
  <header>
    <hgroup>
      <h2>Orange Cat Statue</h2>
      <p>One of a kind!</p>
    </hgroup>
  </header>
  
  <p>This statue is simply the cat's meow.</p>
  
  <footer>
    <button>Buy Now for $1,995.95</button>
  </footer>
</article>

This is groovy! You no longer have “random” selectors and properties in a stylesheet any longer. You are defining a styling API for a component. Any article.is-product in your HTML will use specified styles by default, but you can override them in a variety of ways.

Maybe in certain cases you want a bright orange button!

<article class="is-product" style="--product-button-color: var(--orange-8)"></article>

Maybe on a certain page you want larger product titles!

body.awesome-page article.is-product {
  --product-heading-size: var(--font-size-8);
}

What’s even cooler is you can prototype and test these sorts of changes out right in your browser. You can set --product-heading-size to anything you want within your browser’s dev tools, and see the change reflected immediately. You simply can’t do that with any other design system technology.

That’s the power of CSS Variables when used to create your sophisticated style guide / design system.

✨ Live Demo ✨ #

Nice. But I Don’t Want to Invent All My Design Tokens. Can’t I Just Import Them? #

Yes! The great thing about modern CSS is there’s an increasing number of design token collections out in the wild already. The two I’m most familiar with and like a great deal are:

Open Props #

The crazy thing about Open Props is that it’s not a “framework” or a “design system” or “build tool” per se. It’s literally a collection of variables. That’s it. (There are a few additional features available like a normalize stylesheet and button styling, but that’s all entirely optional.)

Which means it’s sort of a no-brainer to pull Open Props into virtually any project. Sure, you can invent your own typographic scale and your own spacings for margin/padding and your color gradients and your own animation easings and…yeah. That’s a lot of work, right? Or just grab Open Props and save yourself the trouble. It works for me! Might work for you too!

Shoelace #

Shoelace is a fantastic project which provides an excellent set of design tokens like a typographic scale and spacings and colors and whatnot…along with a ton of pre-built components like Alerts and Cards and Dialogs and Inputs and Sliders and Tabs. All free! All open source! All web components which can work with any tech stack! What’s not to love?!

Throw Your Design Tokens into a Particle Accelerator with calc and clamp #

So you have a size token of 2rem and another of 3rem. But you reeeeealy need 2.5rem to make this one part of the design just right. If you also have a 0.5rem token, you can add a dash of math and get just the right value!

section {
  margin-block-start: calc(var(--size-8) - var(--size-2));
}

You can calculate all sorts of values to get new ones, and you can even get into negative numbers! For example, you could set a negative margin using the same variables:

figure {
  margin-inline: calc(0rem - var(--size-4));
}

What about multiplication and division? Yep, once again you’re covered!

article.gigantic {
  padding: calc(var(--content-padding) * 3.5);
}

Variables are also fun to use inside of clamp. Clamp lets you set a starting minimum, a range, and a final maximum for a value. So for example, if you had an image you wanted to scale with the rest of the content but not go above or below a certain width, you could do:

<img alt="Portrait of the Artist" src="/images/portrait.jpg" style="
  width: clamp(200px, 40vw, 600px);
" />

This says “make the width 200px, then go with 40 percent of the viewport width if it’s higher than that, but don’t go beyond 600px”. If we wanted to express this with variables instead, it’s totally doable!

<img alt="Portrait of the Artist" src="/images/portrait.jpg" style="width: clamp(
  var(--portrait-image-min-size),
  var(--portrait-image-range-size),
  var(--portrait-image-max-size)
)" />

clamp works for typography, grids, and many other purposes. Combined with both calc and CSS variables—wow, what a powerhouse.

✨ Live Demo ✨ #

Variant & Responsive Design Tokens #

One trick I like to do a lot these days is assign various style rules to semantic base tokens, then redefine them in variations or using media/container queries. It’s quite powerful!

Let’s say you have a button with a few color variants. We’ll use a data attribute to set variants, and instead of directly fiddling with background-color across all the different variations, we’ll just change our base token!

button {
  /* Set up default, primary, and danger background colors: */
  --button-default-background: var(--sand-6);
  --button-primary-background: var(--indigo-7);
  --button-danger-background: var(--red-7);

  /* Default background: */
  --button-background: var(--button-default-background);

  /* Now assign the semantic base token: */
  background-color: var(--button-background);
}

button[data-variant="primary"] {
  /* Change the base! */
  --button-background: var(--button-primary-background);
}

button[data-variant="danger"] {
  /* Change the base! */
  --button-background: var(--button-danger-background);
}

✨ Live Demo ✨ #

And the same principle can easily apply to media queries as well:

footer {
  /* Set up large and small font sizes */
  --footer-font-size-lg: var(--font-size-6);
  --footer-font-size-sm: var(--font-size-4);

  /* Default to large size */
  --footer-font-size: var(--footer-font-size-lg);

  /* Now assign the semantic base token: */
  font-size: var(--footer-font-size);
}
 
@media (max-width: 640px) {
  footer {
    /* Change the base! */
    --footer-font-size: var(--footer-font-size-sm);
  }
}

Now this may not seem like it gets you that big of a win for simple components, but for more advanced components where there are lots of nested child elements in the internal markup, you can just focus on your tokens in media queries instead of all the various selectors and DOM structure involved.

Wrapping Up #

All right, so by now in your journey you’ve learned all about how to reset (or not) your base HTML styles, how to structure your global stylesheets, and how to work with design tokens.

Now let’s look at how you might apply principles from “classless CSS” themes (or just go grab one!) to build out the basic layouts and templates of your web project.

Next Episode:
Class-Less CSS FTW!