My new ebook  Design Systems for Developers  is here! Start reading

Design

Generating Design Token Theme Shades With Style Dictionary

Last Updated: 2021-02-09

Table of Contents

1 | Introduction to Design Tokens
2 | Managing and Exporting Design Tokens With Style Dictionary
3 | Exporting Design Tokens From Figma With Style Dictionary
4 | Consuming Design Tokens From Style Dictionary Across Platform-Specific Applications
5 | Generating Design Token Theme Shades With Style Dictionary
6 | Documenting Design Tokens With Docusaurus
7 | Integrating Design Tokens With Tailwind
8 | Transferring High Fidelity From a Design File to Style Dictionary
9 | Scoring Design Tokens Adoption With OCLIF and PostCSS
10 | Bootstrap UI Components With Design Tokens And Headless UI
11 | Linting Design Tokens With Stylelint
12 | Stitching Styles to a Headless UI Using Design Tokens and Twind

What You’re Getting Into

A couple of days ago, my dear wife and I watched the movie Tangled. If you haven’t seen it, it’s a Disney movie based loosely on the German fairy tale “Rapunzel.”

One of my favorite scenes is when Rapunzel is early on her adventure to see a display of lateen lights at the nearby kingdom with a thief (and pretty boy) named Flynn Rider.

Flynn takes Rapunzel into what at first seems to be a charming pub with a jolly atmosphere called “The Snuggly Duckling.”

It turns out, the pub is not so charming but full of menacing thugs, including a large gentleman with a hook hand.

After finding out that there is a bounty for capturing Flynn, the thugs seek to get their hands on him. Looking on with anxiety, Rapunzel bursts forth and exclaims that she has a dream. They should let Flynn go and let her press on in her journey towards the kingdom to see the lantern lighting.

After a long pause, the thug with the hook hand bursts into song exclaiming that he too has a dream.

Generating Design Token Theme Shades With Style Dictionary

It is an ironic moment. All this time, you were pulled into thinking that Rapunzel would have to swing her hair and frying pan to get out of the sticky situation. Also, you definitely wouldn’t have thought that the thug with the hook hand had a dream to become greater than Mozart, but that’s how the charming tale goes.

Like “The Snuggly Duckling” and the hook-handed thug, I did not know that there was more to hex codes than a hashtag and six numbers and/or letters.

The six digits are compromised of three sets of two digits. Each set represents red, green, and blue respectively.

100% red + 100% blue + 100% green = white.

0% red + 0% blue + 0% green = black.

Given that, how do you make orange?

Well, I have no idea. I could research and give you the technical details and maybe ask Duolingo to make a special course so that you can translate all the hex codes in your codebase to your peers.

You could memorize some facts and piece them together but it’s hard.

Maybe this shows my naivety. I’m a UI developer, and I’m very creative. However, design is clearly my second language.

To be honest, I’ve never been intrigued to think about why hex codes are the way they are.

Sure, I’ll copy and paste them over and over and over again. Sure, I’ll grab a bunch of them off Coolors to make some slick animated SVG on CodePen. But, I surely won’t even stop to think about why hex codes are the way they are.

Maybe I’m digging a bigger hole in my naivety, or maybe my brain just works differently than my very talented designer friends.

My point is that it takes more than a curiosity to get me into the reasoning behind b3b3b3.

Well, I recently found that something else to draw me in.

I read a great post on why you should consider switching from hex codes to HSL values to style the color palette of your application.

HSL stands for Hue, Saturation, Lightness.

Generating Design Token Theme Shades With Style Dictionary

Hue is not as “huh” as the red in RGB. Hue represents the pigment of color.

Generating Design Token Theme Shades With Style Dictionary

By memorizing just six ranges of degrees between 0 and 360, you can immediately get a sense of what type of color is represented by the HSL value.

Saturation is not a number representing degrees, like hue, but a percentage repainting the purity of a color. More memorably, it can be said that it represents the amount of gray pigment applied to the hue pigment.

Finally, lightness, also representing a percentage, indicates the brightness of a color. Put it another way, it represents the amount of white pigment applied to the hue pigment.

Generating Design Token Theme Shades With Style Dictionary

Here’s what it looks like in your CSS:

color: hsl(33, 80%, 50%);

I have to admit. This is much easier to read and think about than b3b3b3.

That’s pretty cool. But, will I take the time to think about translating these values into color in my brain? Probably not.

It’s cool if you’re brain works like that. I’m just being honest with myself.

The Possibility of Theme Shades Using HSL Values Generated By Style Dictionary

However, I did discover something that tipped me over the edge. It pulled at more than mere curiosity or tidiness.

I learned that since you can control the lightness of a color, you can create lighter (or darker) shades of all your colors by making the saturation or lightness percentage dynamic:

color: hsl(33, 80%, var(--shade));

Do you know what that means? You can toggle between light and dark mode by updating the shade, not by toggling hex codes.

Additionally, it guarantees that the dark colors in dark mode are truly the darkened version of your colors, not arbitrary light and dark color.

That’s a more minor point, however. What is more excited about is that since the dark mode is controlled by a percentage, you can have more than just light and dark mode. You can have a whole range of shades for theming your application!

Generating Design Token Theme Shades With Style Dictionary

Since I’ve been writing about my exploration of design tokens, I pondered on how a design token process could fit with the idea of theme shades.

Then, I remembered…

Style Dictionary exposes a transform option that can translate a hex code into an HSL value.

So, we can automatically generate theme shades such as dim and lights out (using Twitter’s terminology) while providing just the light theme’s color palette as hex codes in the design file.

We also can generate the 000-900 shades of color automatically. 🙀

In this article, we’ll learn how to take hex codes from a Figma file and translate them into design tokens representing theme shades.

Creating The Color Palette

In a previous article, I wrote about creating a process to export a JSON file representing the design tokens from a Figma file into a Style Dictionary repository using the Design Tokens plugin.

However, to make things easier for this article, I created a simple JSON file representing design tokens for a color palette from scratch, following the “Category/Type/Item” format:

// design-tokens.json
{
  "color": {
    "background": {
      "page": {
        "primary": {
          "value": "rgba(152, 94, 255, 1)",
          "type": "color"
        },
        "secondary": {
          "value": "rgba(0, 179, 166, 1)",
          "type": "color"
        },
        "error": {
          "value": "rgba(176, 0, 32, 1)",
          "type": "color"
        }
      }
    }
  }
}

I’ve mentioned this “CTI” format earlier in this series, but I’ll give a quick breakdown of this JSON file.

Generating Design Token Theme Shades With Style Dictionary

In the blob above, color is the category, background is the type, and page is the item. Meaning, the specified colors are for styling the background color of pages in an application. primary, secondary, and error are the different “sub-items,” or variants. I didn’t include a state.

Generating The Shades From Our Design Tokens Using Style Dictionary

The Setup

For now, copy the JSON blob above and save it as a file on your machine called design-tokens.json.

Before we do something with it, it’ll make things easier to have an automated process to transform our hex code colors to HSL colors using Style Dictionary whenever the design-tokens.json file is updated in a style dictionary repository.

Also, we’ll want automation to deliver the HSL deliverables to a sample repository containing a React app where we can attempt to wire-up the theme shades.

As I’ve written about before, we can achieve that automation by using GitHub Actions.

You can fork my style-dictionary repository and create a new branch off of the consuming-design-tokens-from-style-dictionary-with-github-actions branch.

I’ve named this branch after the title of this article: generating-design-token-theme-shades-with-style-dictionary

Basing off of this branch will allow us to pick up where I left off in my previous article.

Our branch on that repository contains an NPM package that has the Style Dictionary CLI as a dependency. It also contains an input folder with a design-tokens.json file which we’ll replace with the new one we just saved.

Lastly, it contains the GitHub Actions workflows that will run the Style Dictionary CLI to transform the design tokens into platform deliverables. After that, it will deliver the platform deliverables to any specified consuming applications.

What We’ll Code

Given this, we’ll need to first update the Style Dictionary config (found in config.json) to transform the color palette design tokens from Figma into CSS variables with HSL values, including all the theme shades.

Then, we’ll update the GitHub Actions workflows to fit our needs, allowing the theme shades to be delivered to a React app.

After that, we’ll be in good shape to kick off this automation by replacing the input/design-tokens.json file in our style dictionary with our new one.

Finally, we’ll update the React app to test out creating theme shades from the HSL color tokens.

Transforming The Hex Code Tokens to HSL

First things first, we’ll modify the config.json file to only create a single platform deliverable called _variables.css that will be written to the output folder:

{
  "source": ["input/**/*.json"],
  "platforms": {
    "css": {
      "transformGroup": "css",
      "buildPath": "output/css/",
      "files": [{
        "destination": "_variables.css",
        "format": "css/variables"
      }]
    }
  }
}

You’ll recall that a “transform group” is a group of pre-defined “transforms,” or steps, to convert the design tokens to a common platform deliverable.

In our case, the css transform group generates CSS variables that can be consumed by an application.

Checking the Style Dictionary documentation, we can see that the pre-defined css transform group performs the following “transforms:”

attribute/cti
name/cti/kebab
time/seconds
content/icon
size/rem
color/css

The color/css transform seems to be relevant to understanding how the exported design tokens will represent our colors.

Generating Design Token Theme Shades With Style Dictionary

It turns out that this transform will export the color design tokens as a hex code, or a rgba.

rgba is the same type of red, green, blue coloring as a hex code but with a number between 0 and 1 driving the transparency.

Appropriately, the transform uses rgba if it detects transparency in the original color design tokens.

Checking the documentation again, we can see that there is another transform called color/hsl.

Sweet, that’s just what we need! 🎉

It will transform a color value in the JSON file representing the original design tokens to an HSL value, or HSLA if there is transparency.

Under the hood, the color/hsl transform just calls the toHslString method exposed by a library called Tiny Color.

Since the transform groups are just cherry-picked transforms, we can cherry-pick our own transforms in place of using a pre-defined transform group.

In our case, we can add a transforms list in the config.json replacing the transformGroup: "css".

It will have all the documented transforms of the css transform group except that we’ll swap out color/css for color/hsl:

{
  "source": ["input/**/*.json"],
  "platforms": {
    "css": {
-      "transformGroup": "css",
+      "transforms": ["attribute/cti", "name/cti/kebab", "time/seconds", "content/icon", "size/rem", "color/hsl"], 
      "buildPath": "output/css/",
      "files": [{
        "destination": "_variables.css",
        "format": "css/variables"
      }]
    }
  }
}

After saving those changes, let’s make sure that the .github/workflows/deliver-tokens-to-consumer.yml file has the correct settings so that our transformed color tokens will be delivered to the correct consuming application.

Here is a starter for an application we can consume: https://github.com/michaelmang/design-token-theme-shades

Now, let’s update the workflow to commit the transformed color tokens to the consuming application.

Here is what the changes looked like for me (you’ll need to tweak the GitHub settings with your own repository and info):

name: Deliver Tokens To Consumer

on:
  push:
    paths:  
      - 'output/**'

jobs:
  # Add a job for every additional consumer/application
  deliver_to_react_application:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v2
    - name: Deliver Platform Deliverable To Application
      uses: andstor/copycat-action@v3
      with:
        # See documentation: https://docs.github.com/en/actions/reference/encrypted-secrets
        # Set the secret in the "src" repository
        personal_token: ${{ secrets.API_TOKEN_GITHUB }}
        # This is the branch that has the code for this article
-       src_branch: consuming-design-tokens-from-style-dictionary-with-github-actions
+       src_branch: generating-design-token-theme-shades-with-style-dictionary
-       src_path: output/scss/_variables.scss
+       src_path: output/css/_variables.css
        dst_owner: michaelmang
-       dst_repo_name: consume-style-dictionary-github-actions
+       dst_repo_name: design-token-theme-shades
-       dst_branch: main
+       dst_branch: implement-shades
-       dst_path: tokens/_variables.scss
+       dst_path: tokens/_variables.css 
        username: michaelmang
        email: mikemangialardi94@gmail.com
        commit_message: Update platform deliverable

Let’s commit and push our changes so far.

Now that the automation will work as expected, we can push our new design-tokens.json file that we saved earlier.

When the design-tokens.json file is pushed to our latest branch on the style dictionary repository, we
should see the GitHub Actions workflows do their work.

First, transforming and exporting the received design tokens file into the transformed color tokens with HSL values in a _variables.css file.

Second, delivering the platform deliverable to our consuming application on a implement-shades branch.

Now, let’s push the design-tokens.json file we saved earlier to the style-dictionary repository and see if it works.

You can investigate the “Actions” tab for that repository on GitHub to see the results of the workflows, but the ultimate check is to make sure that the consuming application got the color tokens:

Generating Design Token Theme Shades With Style Dictionary

Awesome, there is a commit with the color tokens with HSL values to represent the colors. 🎉

Utilizing the HSL Values to Generate Shades

It’s cool that we can transform our RGB values or hex codes into HSL values using Style Dictionary.

However, it would be even cooler if we could leverage Style Dictionary to do an additional transformation to create a range of shades for each color.

Let’s do that next.

What I’m picturing is that we add a new transform to our transforms list in the config.json file of our style dictionary repository.

It doesn’t look like there is a pre-defined transform that can generate shades from an HSL value. However, Style Dictionary offers the flexibility to register a custom transform.

When Style Dictionary builds, it parses the design tokens in your JSON file. Any property within the JSON object that contains a nested value property is internally referred to as a “prop.”

Each registered transform in Style Dictionary is represented as an object:

StyleDictionary.registerTransform({
  name: "time/seconds",
  type: "value",
  matcher: function (prop) {
    return prop.attributes.category === "time";
  },
  transformer: function (prop) {
    return (parseInt(prop.original.value) / 1000).toString() + "s";
  },
});

The example above shows how you can register a custom transform with some Node code.

A transform is an object containing a name, type, matcher, and transformer property.

The pre-defined transforms are also simple objects that follow the same structure.

The matcher and transformer functions provide you a prop which is a design token represented as an object:

{
  prop: {
    value: 'hsl(262, 100%, 68%)',
    type: 'color',
    original: { value: 'rgba(152, 94, 255, 1)', type: 'color' },
    name: 'color-background-page-primary',
    attributes: {
      category: 'color',
      type: 'background',
      item: 'page',
      subitem: 'primary'
    },
    path: [ 'color', 'background', 'page', 'primary' ]
  }
}

The blob above is what we would get if we registered a custom transform in our build process and had the matcher or transform function just log the provided prop.

It is on the “prop” that Style Dictionary does its transforms.

At the end of your chain of transforms, Style Dictionary ultimately formats/translates all your “props” into the platform deliverable. In our case, each prop is formatted into a CSS variable.

This means that to add another CSS variable, or whatever platform deliverable you are working with, you need to add a new prop.

A limitation of transforms is that you can’t clone props.

In a word, we can’t use a transform for generating our shades.

The good news for us is that Style Dictionary does allow you to access the non-transformed properties in Node.

There is another library called tinycolor2, a color manipulation library that Style Dictionary uses under the hood for its color transformations.

We can do the color/hsl transform manually using this library (as there would be no other way to have access to the transformed props all at once before the build).

Then, we can take each color and generate shades by manipulating the HSL values.

A simple algorithm to do this is to tweak the “lightness” from the HSL value. Recall, lightness is the third number in the HSL value. It represents the percentage of white pigment to be applied to the “hue”, the first number. When the lightness increases, we are creating a lighter shade. When the lightness decreases, we are creating a darker shade.

We want to generate as many shades as we can by offsetting the lightness by 10%.

Since the lightness can be anywhere between 1% and 99% (given that 0% is always black and 100% is always white), we can increase the lightness by 10% as long as it is below 100%.

Once we can no longer increase the lightness without exceeding 99%, we will decrease the lightness by 10% from the original value. We do this until the next decrease would be below 1%.

For example, if the lightness was 50%, we would be able to increase by 10% a total of 4 times before another increase would exceed 99%:

original-shade: 50%;
lighter-shade-1: 60%;
lighter-shade-2: 70%;
lighter-shade-3: 80%;
lighter-shade-4: 90%; // + 10% would be 100% which is > 99%

By the time we can no longer increase, we have 5 shades in total. Therefore, we can create 4 darker shades by decreasing the lightness by increments of 10% before another decrease would fall below 1%:

darker-shade-1: 10%;
darker-shade-2: 20%;
darker-shade-3: 30%;
darker-shade-4: 40%;
original-shade: 50%;
lighter-shade-1: 60%;
lighter-shade-2: 70%;
lighter-shade-3: 80%;
lighter-shade-4: 90%;

We can open our style dictionary and create a new file called build.js:

// build.js
const StyleDictionary = require("style-dictionary").extend("./config.json");

StyleDictionary.buildAllPlatforms();

In the code above, we are using the importing StyleDictionary as a module in a way that will allow us to extend the configuration found in config.json.

At the bottom, the StyleDictionary.buildAllPlatforms() will do the equivalent of doing style-dictionary build on the command line.

Now that we’re extending Style Dictionary, we’ll do the build by running this Node file in place of using the CLI:

// package.json
 "scripts": {
-   "build": "style-dictionary build" 
+   "build": "node build.js"
 },

Additionally, we’ll have to remove the automate color/hsl transform since we will do it ourselves:

transforms: [
  "attribute/cti",
  "name/cti/kebab",
  "time/seconds",
  "content/icon",
  "size/rem",
- "color/hsl" 
],

Next, let’s install install tinycolor:

npm i tinycolor

We can replace the contents of build.js with the following:

const StyleDictionary = require("style-dictionary").extend("./config.json");
const tinycolor = require("tinycolor2");

// increase the lightness by 10%
// tinycolor represents the percent as a decimal between 0 and 1)
// for example: hsl(262, 100%, 68%) is represented as { h: 262, s: 1, l: .68 } 
const OFFSET = 0.1;

// the new color properties to replace the original ones
// recall, the original ones are in `design-tokens.json`
let shades = {};

// round to the highest range
// for example: .62 rounds to .70
function roundUp(num, offset = OFFSET) {
  return Math.ceil(num / offset) / 10;
}

// since tinycolor represents the percent as a decimal, translate the decimal to the percentage
function asPercent(num) {
  return num * 100;
}

// appends the shade percentage to the key
// for example: primary + { h: 262, s: 1, l: .68 } becomes primary-70
function asShadeKey(key, lightness) {
  return `${key}-${asPercent(roundUp(lightness))}`;
}

// convert the object representing the hsl back into a string
// for example: { h: 262, s: 1, l: .68 } becomes hsl(262, 100%, 68%)  
function asHslString(ratio) {
  return tinycolor.fromRatio(ratio).toHslString();
}

// add a new color property for the generated shade
function cloneShade({ hsl, key, lightness, prop }) {
  const shadeKey = asShadeKey(key, lightness);
  shades[shadeKey] = {
    ...prop,
    value: asHslString({ ...hsl, l: lightness }),
  };
}

// the original color properties
const colorProps = Object.entries(
  StyleDictionary.properties.color.background.page
);

for (const [key, prop] of colorProps) {
  // convert any color into a hsl object 
  const hsl = tinycolor(prop.value).toHsl();

  // extract the original lightness before we manipulate it
  const { l: originalLightness } = hsl;
  let lightness = originalLightness;

  // add a property for the original shade
  cloneShade({ hsl, lightness, key, prop });

  // add a property for a lighter shade (higher lightness percentage)
  // until another lighter shade would go above 99%
  while (lightness + OFFSET < 1) {
    lightness = lightness + OFFSET;
    cloneShade({ hsl, lightness, key, prop });
  }

  
  // reset lightness to original value
  lightness = originalLightness;

  // add a property for a darker shade (lower lightness percentage)
  // until another darker shade would go below 1%
  while (lightness - OFFSET > 0) {
    lightness = lightness - OFFSET;
    cloneShade({ hsl, lightness, key, prop });
  }
}

// replace the original color properties with all the new shade properties
StyleDictionary.properties.color.background.page = shades;

// build our dictionary for all platforms as specified in the config
// this is the equivalent of: style-dictionary build
// when using the CLI
StyleDictionary.buildAllPlatforms();

Make sure to read the details of how this all works as shown in the comments. It’s easiest to read it from top to bottom.

The TL;DR is that we apply the algorithm that we went over earlier using tinycolor.

tinycolor allows us to represent the HSL value as object, so that we can access and manipulate the hue, saturation, and lightness.

Once we manipulate the lightness to generate a new shade, we can convert the HSL object back into a string.

Also, for every shade that we generated, we append the lightness percentage to the variant key (i.e. primary-70 contains the value of hsl(262, 100%, 68%)).

The result of our script is that output/_variables.css contains additional variables for all the shades of every color originally specified in the design-tokens.json file:

Generating Design Token Theme Shades With Style Dictionary

Woohoo! 🎉

You can run npm run build at any point to test that this is all working locally.

Once you’ve digested the code, push the changes to the branch.

With our GitHub Actions automation, we should see that the consuming application was delivered all of the theme shades!

Generating Design Token Theme Shades With Style Dictionary

Consuming Design Token Theme Shades

Now, we have an automated process of generating theme shades from a single color and delivering them to a consuming application.

From there, the consuming application can do whatever it wants with them. This could be used for having a list of potential color variants that may be used in an application.

Or, it can be used for toggling a broader spectrum of themes than the traditional “light” and “dark” themes.

The point of this article is to focus on how to generate design token theme shades with Style Dictionary and not to iterate through all the ways you could consume those theme shades.

However, I’ll provide a basic example to whet your appetite.

Generating Design Token Theme Shades With Style Dictionary

You can view the final code for my demo here:
https://github.com/michaelmang/design-token-theme-shades/pull/2

That’s all folks! 🎉

My next article will be on publishing a design documentation site using your design tokens.

Until then, pow, share, and discuss!

Design Systems for Developers

Read my latest ebook on how to use design tokens to code production-ready design system assets.

Design Systems for Developers - Use Design Tokens To Launch Design Systems Into Production | Product Hunt

Michael Mangialardi is a software developer specializing in UI development with React and fluent in UI/UX design. As a survivor of impostor syndrome, he loves to make learning technical skills digestible and practical. Formerly, he published articles, ebooks, and coding challenges under his brand "Coding Artist." Today, he looks forward to using his mature experience to give back to the web development community. He lives in beautiful, historic Virginia with his wife.