TL;DR: If you’re running into the problem of having to unify a complex design that spans across multiple platforms and technologies, Amazon Style Dictionary is what you need.

Not that long ago I had my first encounter with design system project that spans multiple platforms. In this case it was just multiple web-based applications (React, Angular, React Native) that needed updating, but I’m pretty sure this approach will work if you have native iOS & Android code too.

The Problem

The problem was simple: the visual identity has been updated by a marketing agency and the product needed a new coat of paint, too. The issue — legacy application in Angular (ingesting Less) and React application ingesting JavaScript variables into styled-components to make things work. Both of the applications needed to get restyled into the new visual identity and also kept in sync in case something changes. Just a couple of years ago, it would mean having two codebases that needed to be kept in sync meaning exactly 100% more work when updating anything. We had two options:

  • Option 1: Rewrite everything into React. Definitely not feasible at this stage and meaning pulling off almost entire engineering team off feature work and making them rewrite pretty complex Angular application to React. Good luck with selling that to the team leads.
  • Option 2: Create an abstraction layer above the styles that generates legacy Less code for Angular and JS variables for the React applications. Obvious choice if you’re strapped for resources.

The Solution

The solution for anyone who ever worked with a design system that’s bigger than a couple of elements in this case is pretty obvious: Design Tokens. Design Tokens (sometimes known as Quarks in Atomic Design), for those who haven’t heard about them before, are the tiniest pieces of information that elements of your UI are made of — spacing, sizes, colors and so on. In my research, I came across two scalable ways to handle tokens: Theo, by Salesforce UX Team and Style Dictionary by Amazon. In this case I ended up with Style Dictionary for technical reasons and the fact that the JSON formats that Style Dictionary uses ended up being easier to grasp for the development team who was always JavaScript-focused.

The Tokens

In the end, I moved all the colors, spacings and sizes out to Amazon Style Dictionary files. To make it even simpler for the development team, the tokens are organized into brand tokens (with raw color definitions) and tokens based on the use case, which are just aliases for brand tokens. That way, the developers don’t have to think “Was the disabled button background Gray 01 or Gray 02?”, they can just simply default to a token called color-button-disabled-background.

File structure

File structure follows the use cases, so tokens is the main directory for the tokens, which are later grouped by the token type (size, color) that include files defining brand styles and per-component styles, e.g. brand.json, error.json, input.json and so on. Every component JSON file is built off of branding colors.

Example token files

Brand token files is the base for everything. Below you can see an example of the brand color tokens:

{
  "color": {
    "brand": {
      "purple": {
        "base": { "value": "#3529BE" },
        "dark": { "value": "#071B5A" }
      },
      "teal": {
        "base": { "value": "#39CDBC" },
        "light": { "value": "#E1F8F5" }
      },
      "white": { "value": "#FFFFFF" },
      "gray": {
        "01": { "value": "#212C52" },
        "02": { "value": "#707580" },
        "03": { "value": "#C2C5CC" },
        "04": { "value": "#DFE1E6" },
        "05": { "value": "#F4F6FD" },
        "06": { "value": "#FAFBFF" }
      }
    }
  }
}

Once this is defined, components can simply refer to brand tokens and designers can update only brand tokens and all the component tokens based on it will be regenerated:

{
  "color": {
    "input": {
      "text": {
        "background": { "value": "{color.brand.white.value}" },
        "border": { "value": "{color.brand.gray.04.value} " },
        "text": { "value": "{color.font.base.value}" },
        "disabled": {
          "background": { "value": "{color.brand.gray.05.value}" },
          "border": { "value": "{color.brand.gray.04.value}" },
          "text": { "value": "{color.brand.gray.02.value}" }
        },
        "error": {
          "background": { "value": "{color.brand.white.value}" },
          "border": { "value": "{color.error.base.value}" },
          "text": { "value": "{color.error.base.value}" }
        }
      }
    }
  }

The benefit of using tokens that way, beyond developers having an easier time using it, is that if brand changes again or there’s requirement to theme the system, you can simply replace the brand.json with a different file and all the components will be regenerated.

Writing own transforms

By default, Style Dictionary bases sizes off rem value. This is pretty good, except if the designers are more comfortable using pixels (since that’s the language of Sketch & Figma). Style Dictionary doesn’t have a pixels-to-rems transform, but it’s actually really easy to write your own transforms if you know a bit of JavaScript. In this case, it would be:

const BASE_FONT_SIZE = 16;

StyleDictionary.registerTransform({
  name: "size/pxToRem",
  type: "value",
  matcher: function(prop) {
    return prop.attributes.category === "size";
  },
  transformer: function(prop) {
    return `${prop.value / BASE_FONT_SIZE}rem`;
  }
});

After that, you can use size/pxToRem exactly like built-in Style Dictionary transforms.

Deploying to application

Once we had all the tokens moved out to JSON files, we ended up figuring out all the transforms and generating a config:

{
  "source": ["style-dictionary/tokens/**/*.json"],
  "platforms": {
    "less": {
      "transforms": [
        "attribute/cti",
        "name/cti/kebab",
        "time/seconds",
        "content/icon",
        "size/pxToRem",
        "color/css"
      ],
      "buildPath": "legacy_app/css/tokens/",
      "files": [
        {
          "destination": "colors.less",
          "format": "less/variables",
          "filter": {
            "attributes": {
              "category": "color"
            }
          }
        },
        {
          "destination": "sizes.less",
          "format": "less/variables",
          "filter": {
            "attributes": {
              "category": "size"
            }
          }
        }
      ]
    },
    "react": {
      "transforms": [
        "attribute/cti",
        "name/cti/constant",
        "size/px",
        "color/hex"
      ],
      "buildPath": "react_app/styles/tokens/",
      "files": [
        {
          "destination": "colors.js",
          "format": "javascript/module",
          "filter": {
            "attributes": {
              "category": "color"
            }
          }
        },
        {
          "destination": "sizes.js",
          "format": "javascript/es6",
          "filter": {
            "attributes": {
              "category": "size"
            }
          }
        }
      ]
    }
  }
}

And regenerating the current configuration files for the UI to make sure the colors and sizes match the newly minted tokens, which was a bit of manual labor but nothing a well-executed Find & Replace can't do:

@text-color: @color-text-default;
@ui-dark-purple: @color-brand-purple-dark;

Next Steps: Transforming colors with Color.js & Generating Figma Palettes

In the process of working with the Style Dictionary files, I found at least a couple of places where it would be even better to create a new color by manipulating the base color. Good example of that are :hover and :active states for links and buttons, where it’s often the case that button with a hover/focused state is supposed to be 5% lighter and clicked button is 5% darker. I’m currently researching how to use Color.js to do those transforms on build and will report back once I figure it out.

I also would love to have Figma palettes & components based on the current state of the tokens instead of having to redraw them manually, so I'll have to look into Figma API if that's even possible and then try and sell it to someone who's way better at writing code.

And voila! In a couple of days of research and experimenting, we had both legacy and new application styled consistently across the board.

Resources

If you are facing a similar problem, here are a few other resources to get you started:

Hope this article was helpful. If you have any other questions, you can reach out on Twitter, where I hang out way more than I probably should.

(photo by Magda Ehlers)