How to manage your Design Tokens with Style Dictionary

Cristiano Rastelli
27 min readSep 27, 2018

--

Recently I have come across a tool, called Style Dictionary, developed by Danny Banks at Amazon, that allows you to manage design tokens (or “style properties”) for a design system. I’ve used it to replace the existing tool that we used to process the design tokens for our Cosmos design system. I’ve found the task very interesting, and I’ve decided to document it, so that other people may benefit from what I’ve learned during the process.

Notice: I’ve tried to be as comprehensive as possible, which means this blog post is extremely long. I know, I know… ¯\_(ツ)_/¯.

A reader, looking at the “24 min read” at the top of this page.

If you just want to look directly at the code, I have created a GitHub repo with a configuration and a setup similar to what we’ve used in our project:

➡️ https://github.com/didoo/style-dictionary-demo ⬅️

Go there and look at the code, it should be quite straightforward. If you want, clone the project and use it as a starting point (but keep in mind that is quite opinionated, and may not suit your particular context and needs).

If instead you want to understand a little bit more about Style Dictionary and how to configure it, and also see what were the technical reasons that made me adopt this tool, the choices I made and the decisions I took while introducing it in our design system, then please read on.

Before beginning, one disclaimer: my experience with Style Dictionary is limited to the setup and customisation of design tokens within our design system. For a more detailed explanation of what it can do, its features and APIs, its advanced capabilities, please refer to the official documentation. Also, all the credits for this impressive project go to its contributors and in particular to Danny Banks.

About Design Tokens

In a design system, people often use special entities called “design tokens” to store their “design decisions”. These entities are in the shape of key/value pairs, saved in specific file formats (usually JSON or YAML). These files are later used as input files, processed and transformed to generate different output files, with different formats, to be included and consumed by other projects and codebases. (If you want to know more about design tokens, I suggest to read here, here, here and here).

This, for example, is how we use some design tokens as Sass variables in the declaration of the styles for the <Button/> component:

.button {
position: relative;
display: block;
width: 100%;
min-height: $token-button-height;
margin: 0;
padding: $token-spacing-sm $token-spacing-xxlg;
border-radius: $token-button-border-radius;
text-align: center;
}

// variants

.button--stroke {
border: $token-button-border-width solid currentColor;
background: transparent;
}

.button--monochrome {
border: $token-button-border-width solid $token-colour-gray;
background: #fff;
color: $token-colour-black;
}

// and other variants here
...

These properties are also exposed in our style guide so that they can be part of our documentation:

A portion of the “Tokens” page in our Cosmos design system

Migrating from Theo

When one year ago we introduced the design tokens in our design system, at that time the state of the art for design token management was Theo, a tool developed by the design system team at Salesforce — mainly Adam Putinski and Kaelig Deloumeau-Prigent (now at Shopify) — to manage their impressively large number of design tokens in the Lightning Design System.

At that time, the available version was Theo5, and this is how our design tokens declarations looked like (until a few weeks ago):

{
"global": {
"type": "token",
"category": "brick"
},
"aliases": {
"TOKEN_BRICK_SIZE_SM": {
"value": "70px"
},
"TOKEN_BRICK_SIZE_MD": {
"value": "100px"
},
"TOKEN_BRICK_SIZE_LG": {
"value": "120px"
}
},
"props": {
"TOKEN_BRICK_SIZE_XXSM": {
"value": "36px"
},
"TOKEN_BRICK_SIZE_SM": {
"value": "{!TOKEN_BRICK_SIZE_SM}"
},
"TOKEN_BRICK_SIZE_MD": {
"value": "{!TOKEN_BRICK_SIZE_MD}"
},
"TOKEN_BRICK_SIZE_LG": {
"value": "{!TOKEN_BRICK_SIZE_LG}"
},
"TOKEN_BRICK_SIZE_XLG": {
"value": "150px"
}
}
}

Since then, Theo has evolved a lot (now we are at Theo8!) and the way you declare the token values has quite changed, but what remains a pain point (at least, for me) is the fact that in order to reference a value of a token in another token, you have to declare it before using a specific alias definition; and, if this declaration is in another file, you have to import the file where it’s used (which also means that, when declaring the files to import/process, the order of the declarations matters).

This way to define “aliases” leads to a lot of repetitions (see above, where I had to declare twice TOKEN_BRICK_SIZE_SM/MD/LG, as an alias and as a prop, or here for example). Besides, in this way, the resulting organisation of the design tokens, and the files where they are stored, tends to be very prescriptive (see for example here or here) and rigid when you need to refactor or re-arrange the design tokens.

I also made some tests, to associate custom meta-data to the tokens (to be used as documentation), but I couldn’t find a way to have this information exposed in the generated files. So I had to give up and leave the documentation for our tokens out of our style guide.

Don’t get me wrong: Theo is a great tool, and the issues I have mentioned above are small details, just a matter of personal preferences. (So don’t make your choices based on my impression. Always evaluate a tool for what it can do for you, how it solves your problems, in the context of your needs!).

Anyway, since the first time I implemented our design system’s tokens with Theo, I felt that our setup was OK… but not exactly what I wanted. There was always something I would have done slightly differently, but I couldn’t (because of these technical limitations, and because of the limits in my comprehension of the tool, probably).

The fact remains that I was not 100% satisfied. In the back of my mind, I always had this idea of replacing Theo with a custom-built tool. But lack of time (and hesitation to invest resources in such a complex task) made me reconsider this idea, and continue to keep the existing setup.

First impact with Style Dictionary

I don’t remember exactly where I read for the first time of Style Dictionary, probably on my Twitter feed, or in some Medium blog post, or in a chat in the Design Systems Slack channel (Shaun Bent rightly made me notice that it was announced on February this year, but I don’t remember that post).

Looking at the history of the project on GitHub it seems they have worked on it for a long time, but I’ve never heard about it until a couple of months ago.

I remember I quickly looked at the project website, I understood it was somehow related with design tokens but I didn’t understand exactly how (probably because of the use of the term “style properties” in lieu of the more common “design tokens”).

I also remember this illustration, that caught my eye:

How the build process of Style Dictionary works under the hood (source)

This “deep merge” of different JSON files (plus, the different compilations for different platforms) was exactly what I had in mind.

At that time I didn’t have time to investigate, so I starred the repo and promised myself to have a look in detail later. One day that I didn’t have that much to do, I made a quick test to see how Style Dictionary was supporting this “deep merge” of properties in the tokens’ JSON files. The results were exactly what I was hoping for: if you attach any extra attributes to a JSON key/value pair in Style Dictionary, these meta-informations are seamlessly carried over in the processed files in output. Bingo!

I created a ticket in my backlog—DO-132 — Evaluate Style Dictionary for the generation of the Design Tokens — and went on with the ordinary day-by-day components-related tasks.

Then a couple of weeks later a tweet caught my eyes:

I immediately looked at the slides of the presentation, and fell in love with the project: I wanted absolutely to try it as soon as possible.

At the first opportunity I had, I started to work on that ticket. Two days later, an entirely updated version of our design tokens system, built on Style Dictionary, was in production.

As I said above, given how simple and enjoyable it was for me to adopt Style Dictionary, I started to wonder if it was not the case to write a blog post about the experience, highlighting the reasons that made me decide to switch to it, but also what I learned during the process. So here below are my findings.

Initial setup of Style Dictionary

Compared to other tools, I found it very easy to be up and running using Style Dictionary: in less than five minutes, I was already building my new design tokens.

My suggestion is to create a folder, where you want to store your tokens, install the package, and then run the command:

style-dictionary init basic

This will create a basic project with some example JSON files and the config.json file required to process the tokens and generate the different formats of files in output (see later for more details).

In my case, I did it directly in my existing /tokens folder, because the default structure of Style Dictionary was different from the folder structure of my Theo implementation (Style Dictionary uses properties and build, I used src and dist, so there were no risks of conflicts). After the initial setup, I renamed my existing folders tokens/src.old and tokens/dist.old, and the new folders tokens/src and tokens/dist and updated the config.json file to reflect the new paths. In this way, I was able to continually compare the files generated with Theo and the ones generated with Style Dictionary, and see if there were differences. What I wanted to achieve was to seamlessly replace the tokens generated using Theo with the ones generated using Style Dictionary, without requiring any change in the code consuming the tokens (in our case, our Component Library in React, and our Mobile Web application).

Now, once your project is set up, all you have to do is run the “build” command in your CLI:

style-dictionary build

This will “transform” your JSON files and generate the resulting token files according to the options declared in your config file.

Using the pre-defined build command in your CLI is the simplest way to process your tokens, but you can also decide to implement a custom build script if you want or need (more on this later).

Using Style Dictionary for your own project

At this point, you can start to play around with your token’s JSON files (and you config file) to see the different options that you have, in term of the organisation of your style properties and generation of different output formats. If you want, you can look at the examples folder included in Style Dictionary’s project repository to see more complex organisations and use cases, but I suggest to start with the “basic” one to avoid getting lost in the more advanced features offered by Style Dictionary (e.g. the distribution of assets like fonts or icons as “tokens” or the generation of tokens for React Native projects).

Documentation

Style Dictionary is a very well documented project. You can use the official website available at this address: https://amzn.github.io/style-dictionary/ or you can read directly the markdown files from the GitHub repo: https://github.com/amzn/style-dictionary/tree/master/docs.

I have found very useful — and I strongly suggest to do the same — to play and experiment with the properties’ JSONs and the config files, trying to understand how the whole system works, and in parallel to read the documentation when something is not 100% clear or obvious. Following this approach in just a few hours I’ve found myself able to customise the entire set of my design tokens and create a custom build script.

The ”Category > Type > Item” classification

Style Dictionary has an implicit classification of the tokens:

This classification is simply based on the nesting of the properties inside the JSON files. If we look at this code, for example:

{
"button": {
"border": {
"width": {
"value": "1px"
}
}
}
}

when we process it via the build command the resulting JSON is this:

{
"button": {
"border": {
"width": {
"name": "button-border-width",
"value": "1px",
"original": {
"value": "1px"
},
"attributes": {
"category": "button",
"type": "border",
"item": "width"
},

"path": [
"button",
"border",
"width"
]
}
}
}
}

As you can see, the order in the nesting of the source JSON is automatically interpreted as logical tree/structure, and it’s used to build the name of the properties (e.g. $button-border-width and BUTTON_BORDER_WIDTH in this example) but also to associate them a set of predefined “CTI” attributes.

While working with Style Dictionary, you will find this implicit CTI classification consistently across many helpers and functions. Luckily this is not something that you have to strictly follow (and this says a lot about the quality of the project and the cleverness of its authors). According to the documentation “you can organize and name your style properties however you want, there are no restrictions.”. It’s important anyway to keep in mind this inner trait of this tool, so you can use it correctly, or work around it, if you need/want (that’s what I have done in my project, because I preferred to have a different taxonomy for our design tokens).

My design tokens organisation

Cosmos, our design system, has been built to support different “platforms” (Mobile Web, iOS and Android) and different “brands” (our application has white-label products).

The Cosmos component library, built in React, is entirely adopted by our Mobile Web application, while for the native platforms it works as a “reference” on how to break down the UI in components (for now; in the future we may consider a React Native version of the components, in which case the native platform could start to consume the Cosmos components too).

Conversely, the Cosmos design tokens are consumed by all our platforms. For this reason, we have organised our design tokens accordingly to the way the values of the tokens are related to the different brands and platforms that we need to support. Some of them depend on the brand (e.g primary and secondary colors), some depend on the platform (e.g. font-family and button-height) and some are instead global, which means they have the same value for every brand and platform, (e.g. color-facebook or spacing-small).

Here is a simplified schema of the folder structure of our JSON source files:

├── config.json
├── src/
│ ├── brands/
│ │ │── brand_#1/
│ │ │ ├── color.json
│ │ │── brand_#2/
│ │ │ ├── color.json
│ │ │── brand_#3/
│ │ │ ├── color.json
│ ├── globals/
│ │ │── base/
│ │ │ ├── index.json
│ │ │── color/
│ │ │ ├── basic.json
│ │ │ ├── grayscale.json
│ │ │ ├── features.json
│ │ │ ├── social.json
│ │ │── icon/
│ │ │ ├── index.json
│ │ │── ...
│ │ │── spacing/
│ │ │ ├── index.json
│ │ │── typography/
│ │ │ ├── index.json
│ ├── platforms/
│ │ │── android/
│ │ │ ├── button.json
│ │ │ ├── ...
│ │ │ ├── typography.json
│ │ │── ios/
│ │ │ ├── button.json
│ │ │ ├── ...
│ │ │ ├── typography.json
│ │ │── mobile_web/
│ │ │ ├── button.json
│ │ │ ├── ...
│ │ │ ├── typography.json

Using this organisation, it is quite easy and obvious where to find the different tokens, where to add a new token when needed, or which files to remove if a component is removed from the library.

When it came to the organisation of the destination/output folder, I’ve preferred a structure where it’s clear and obvious for the different “consumers” where to find the files they are interested in.

This is the schema of the folder structure for our generated files:

├── dist/
│ │── android/
│ │ ├── ...
│ │── ios/
│ │ ├── ...
│ │── mobile_web/
│ │ │── brand_#1/
│ │ │ ├── **.scss
│ │ │ ├── **.js
│ │ │ ├── **.json
│ │ │── brand_#2/
│ │ │ ├── **.scss
│ │ │ ├── **.js
│ │ │ ├── **.json
│ │ │── brand_#3/
│ │ │ ├── **.scss
│ │ │ ├── **.js
│ │ │ ├── **.json
│ │── style_guide/
│ │ ├── android_brand_#1.scss
│ │ ├── android_brand_#1.json
│ │ ├── android_brand_#2.scss
│ │ ├── android_brand_#2.json
│ │ ├── ...
│ │ ├── ios_brand_#1.scss
│ │ ├── ios_brand_#1.json
│ │ ├── ios_brand_#2.scss
│ │ ├── ios_brand_#2.json
│ │ ├── ...
│ │ ├── mobile_web_brand_#1.scss
│ │ ├── mobile_web_brand_#1.json
│ │ ├── mobile_web_brand_#2.scss
│ │ ├── mobile_web_brand_#2.json
│ │ ├── ...

As you can see, if I am an Android developer, I will immediately know that my tokens are stored in the /Android folder, and here I will find only the format I am interested in (e.g. XML files). If I am a Mobile Web developer, I will find my files under the /Mobile_Web folder, (SCSS, JS and JSON files, depending on what I need).

The /style_guide folder instead is slightly different, because in this case the tokens are consumed by the Cosmos style guide, and we need to expose the token values for all the possible combinations of platform/brand. The user can select them in the UI, so we need to dynamically load the corresponding token values depending on the selected combination, and in this case a flat structure with all the files under a single folder is better.

In our style guide, the user can switch between different platforms and brands.

Now, when it comes to the actual tokens’ values declarations inside the files, I’ve decided to arrange them in alignment with the actual components they were referring to. Instead of following the suggested CTI — Category > Type > Item — organisation (e.g. color > background > button) I’ve preferred to follow a classification which is more “component-oriented”.

I’ll give you a couple of examples.

Let’s consider the Input component. This is how it looks like in our style guide:

As you can see above, the <Input/> component is differentiated in three specialised components: <Choice/>, <Toggle/> and <Search/>. And this is how the files for these components are organised in our codebase:

├── component-library/
│ │ ...
│ ├── gift/
│ ├── icon/
│ ├── input/
│ │ ├── Input.js
│ │ ├── Input.scss
│ │ ├── Choice.js
│ │ ├── Search.js
│ │ ├── Toggle.js
│ ├── mark/
│ ├── modal/
│ │ ...

To be consistent with this structure, the design tokens for this group of components are saved in tokens/src/globals/input/index.json and the values are declared using this nested format:

{
"input": {
"choice": {
"size": {
"value": "24px"
}
},
"toggle": {
"width": {
"value": "40px"
},
"height": {
"value": "24px"
}
},
"search": {
"height": {
"value": "32px"
}
}
}
}

The generated variables, that are then used in the source code for these components, preserve this ‘no-brainer’ naming convention:

$token-input-choice-size: 24px;
$token-input-toggle-width: 40px;
$token-input-toggle-height: 24px;
$token-input-search-height: 32px;

(As you may have noticed, my generated variables have a “token” prefix. I’ll explain more on this later).

Another important result of this kind of organisation is how it’s easy to use “aliases” or refer to other token values from within different components.

For example, for the brands’ primary and secondary colors, I have not declared them as tokens, but as aliases:

{
"alias": {
"colour": {
"brand": {
"primary": {
"value": "#bada22"
},
"secondary": {
"value": "#c0ffee"
}
}
}
}
}

In this way I was able to use them directly in the /global color declarations:

{
"colour": {
"primary": {
"value": "{alias.colour.brand.primary.value}"
},
"secondary": {
"value": "{alias.colour.brand.secondary.value}"
},
...
}
}

Similarly, I can reference the value of another component’s token inside a different component.

For example, this is the file for the <Icon/> component:

{
"icon": {
"size": {
"xsm": { "value": "10px" },
"sm": { "value": "16px" },
"md": { "value": "22px" },
"lg": { "value": "30px" },
"xlg": { "value": "36px" },
"xxlg": { "value": "46px" }
},
"jumbo-size": {
"md": { "value": "{brick.size.md.value}" },
"lg": { "value": "{brick.size.lg.value}" }
}
}
}

where this is the file for the <Brick/> component, that contains the actual token values:

{
"brick": {
"size": {
"xxsm": {...},
"sm": {...},
"md": { "value": "100px" },
"lg": { "value": "120px" },
"xlg": {...}
}
}
}

As you can see, I have been able to express the correlation (decided by the designers) between the size of the Brick element and the ‘jumbo’ size of the Icon element, in a very simple, clear and — above all — semantic way.

Now, not only Style Dictionary allows this seamless management of the “aliases” values, but, even better, the order in which the files are imported is not relevant in term of resolution of the aliases!

This means you can have complete freedom in how you want to organise the tokens for your project. I suggest to play around with the structure of your design tokens and find the configuration that better suits your needs.

One last thing, before moving to the actual build process of the declared design tokens. As I mentioned before, one feature that you get “for free” with Style Dictionary, is that every attribute that you associate to a property in a source JSON file is transparently and automatically passed down along the transformations and will appear in the output files.

For example, if you write this declaration:

{
"button": {
"meta": {
"description": "design tokens for the button component"
},
"border": {
"width": {
"value": "1px",
"documentation": {
"comment": "the width of the border"
}
}
}
}
}

the generated JSON file is this:

{
"button": {
"meta": {
"description": "design tokens for the button component"
},
"border": {
"width": {
"value": "1px",
"documentation": {
"comment": "the width of the border"
},
"original": {...},
"name": "button-border-width",
"attributes": {...},
"path": [...]
}
}
}
}

As you can see, whatever you add in term of extra attributes to the original JSON files is preserved and passed down during the build process (of course, if the format of the generated file supports this kind of representation).

This means two important things: first, you can use it to associate meta-information to your tokens (for example, I use it to add documentation, comments and notes to them, to be shown in the style guide); second, you can create custom actions or transforms in your build process, where you can use these extra attributes to selectively decide if/how to process the values.

One last cool thing: if you simply add a comment property to a value, this is automatically added as a comment in the exported Scss file (actually, all the file formats that support comments):

{
"button": {
"border": {
"width": {
"value": "1px",
"comment": "this is a comment"
}
}
}
}

this will generate in output:

$button-border-width: 1px; // this is a comment

Useful if you want to export some kind of minimal documentation associated with your design tokens.

This is just one of many “cool” small details that make this library so well thought: I invite you to read the documentation, look at all the available options, and you will find many other gems like this.

You’re halfway through… keep going!

My (custom) build process

As I said above, the simplest way to build your design tokens with Style Dictionary is to run in the command line:

style-dictionary build

You can pass modifiers to the command, to specify the path of the config file and the platform you want to build.

Another possible way to build the tokens is using a Node script. Here as well you can build all the platforms with a simple command:

const SD = require('style-dictionary').extend('config.json');SD.buildAllPlatforms();

or decide to build only specific ones using a similar method:

SD.buildPlatform('web');

Now, as you can see from the code above, all the specifications of how to build the tokens are contained in a single place: the config file. Here is where you declare your source files, your destination folders, your platforms, your formats and the transformations that you want to apply to each token.

Style Dictionary comes with a set of pre-defined transform groups, (e.g web, scss, less, html, android, ios, assets). As the name says, they are just a group of transforms, which in turn are nothing more than functions that “transform a property so that each platform can consume the property in different ways”.

Some of these transform functions are applied to the name of a token (e.g. name/cti/camel or name/cti/kebab), some to its value (e.g. color/hex or size/rem or time/seconds) and some to its attributes (e.g. attribute/cti).

Now, because these functions operate directly on the name, value and attributes of a token, it means that they are somehow coupled with the way you want to declare your properties and values. For example, for some reason you could declare a “time” value with or without the unit of measure (it depends on who will consume your tokens, and how they will expect the data formatted), but the time/seconds transform “assumes a time in milliseconds and transforms it into a decimal”, which means you can use it only if you use that specific input format. In a similar way, almost all the size/** transforms assume a number as an input value, and are applied only to tokens that match the category === 'size' condition (which is not necessarily your use case).

Now, as explained above, I wanted to have a very specific organisation of the generated files for the different platforms: not only have specific formats for each one of them (Web, iOS and Android) saved in their corresponding folder, but I also needed all the possible combinations/permutations of platform and brand made available for the style guide.

I also wanted to have complete freedom in how I organised and declared the key/values properties for the tokens, to be able to manipulate them at will.

So I decided to venture in creating a custom build script leveraging the APIs made available by the Style Dictionary library. I started out of the example described in the documentation

const StyleDictionary = require('style-dictionary');const styleDictionary = StyleDictionary.extend( 'config.json' );// You can also extend with an object
// const styleDictionary = StyleDictionary.extend({ ... });
// you can call buildAllPlatforms or buildPlatform('...')
styleDictionary.buildAllPlatforms();

Reading the code, it was immediately clear to me what options I had: I could refer to an external config.json file or pass a custom object to the extend method; and I could build all the platforms at once via buildAllPlatforms or be more granular and use buildPlatform, with possibly a different configuration object for each platform.

With just these two options, it didn’t take me long to create my first build script. After a few attempts and iterations, my build script looked like this:

const StyleDictionaryPackage = require('./build-style-dictionary-package');
const getStyleDictionaryConfig = require('./build-style-dictionary-config');
console.log('Build started...');// PROCESS THE DESIGN TOKENS FOR THE DIFFEREN BRANDS AND PLATFORMS['web', 'ios', 'android'].map(function(platform) {
['brand#1', 'brand#2', 'brand#3'].map(function(brand) {
console.log('\n======================================');
console.log(`\nProcessing: [${platform}] [${brand}]`);
const StyleDictionary = StyleDictionaryPackage.extend(getStyleDictionaryConfig(brand, platform)); if (platform === 'web') {
StyleDictionary.buildPlatform('web/js');
StyleDictionary.buildPlatform('web/json');
StyleDictionary.buildPlatform('web/scss');
} else if (platform === 'ios') {
StyleDictionary.buildPlatform('ios');
} else if (platform === 'android') {
StyleDictionary.buildPlatform('android');
}
StyleDictionary.buildPlatform('styleguide');
console.log('\nEnd processing'); })
})
console.log('\n======================================');
console.log('\nBuild completed!');

What I am doing here is simple: I am looping through all the platforms and brands that I have; for each combination, I am running a specific build, which receives a dynamic config object, with platform and brand as parameters.

The getStyleDictionaryConfig is no more than a function, that returns the config object specific for that platform + brand combination:

function getStyleDictionaryConfig(brand, platform) {
return {
"source": [
`src/brands/${brand}/*.json`,
"src/globals/**/*.json",
`src/platforms/${platform}/*.json`
],
"platforms": {
"web/js": {
"transformGroup": "tokens-js",
"buildPath": `dist/web/${brand}/`,
"files": [
{
"destination": "tokens.es6.js",
"format": "javascript/es6"
}
]
},
"web/json": {
"transformGroup": "tokens-json",
"buildPath": `dist/web/${brand}/`,
"files": [
{
"destination": "tokens.json",
"format": "json/flat"
}
]
},
"web/scss": {
"transformGroup": "tokens-scss",
"buildPath": `dist/web/${brand}/`,
"files": [
{
"destination": "tokens.scss",
"format": "scss/variables"
}
]
},
"styleguide": {
"transformGroup": "styleguide",
"buildPath": `dist/styleguide/`,
"files": [
{
"destination": `${platform}_${brand}.json`,
"format": "json/flat"
},
{
"destination": `${platform}_${brand}.scss`,
"format": "scss/variables"
}
]
},
... // more declarations here
}
};
}
module.exports = getStyleDictionaryConfig;

Once I saw that this “proof of concept” worked, I started to tinker around the configuration object and see how much I could push the customisation of the design tokens to suit my very own specific “needs”. The results were beyond all possible expectations: using — once again — the APIs made available by Style Dictionary, in no time and with a minimal effort I was able to introduce custom formats, custom transforms and transform groups, and later on even custom template files, tuning every single detail in the generated files.

At the end of this process, my dynamic configuration file looked like this:

function getStyleDictionaryConfig(brand, platform) {
return {
"source": [
`src/brands/${brand}/*.json`,
"src/globals/**/*.json",
`src/platforms/${platform}/*.json`
],
"platforms": {
"web/js": {
"transformGroup": "tokens-js",
"buildPath": `dist/web/${brand}/`,
"prefix": "token",
"files": [
{
"destination": "tokens.es6.js",
"format": "javascript/es6"
}
]
},
"web/json": {
"transformGroup": "tokens-json",
"buildPath": `dist/web/${brand}/`,
"prefix": "token",
"files": [
{
"destination": "tokens.json",
"format": "json/flat"
}
]
},
"web/scss": {
"transformGroup": "tokens-scss",
"buildPath": `dist/web/${brand}/`,
"prefix": "token",
"files": [
{
"destination": "tokens.scss",
"format": "scss/variables"
}
]
},
"styleguide": {
"transformGroup": "styleguide",
"buildPath": `dist/styleguide/`,
"prefix": "token",
"files": [
{
"destination": `${platform}_${brand}.json`,
"format": "json/flat"
},
{
"destination": `${platform}_${brand}.scss`,
"format": "scss/variables"
}
]
},
"ios": {
"transformGroup": "tokens-ios",
"buildPath": `dist/ios/${brand}/`,
"prefix": "token",
"files": [
{
"destination": "tokens-all-generic.plist",
"template": "ios/generic.plist"
},
...
]
},
"android": {
"transformGroup": "android",
"buildPath": `dist/android/${brand}/`,
"prefix": "token",
"files": [
{
"destination": "tokens-all-generic.xml",
"template": "android/generic"
},
...
]
}
}
};
}

The custom functions are all declared in the “package” file, which looks like this:

const StyleDictionaryPackage = require('style-dictionary');// === CUSTOM FORMATS ===StyleDictionaryPackage.registerFormat({
name: 'json/flat',
formatter: function(dictionary) {
return JSON.stringify(dictionary.allProperties, null, 2);
}
});
// === CUSTOM TRANSFORMS ===StyleDictionaryPackage.registerTransform({
name: 'size/pxToPt',
type: 'value',
matcher: function(prop) {
return prop.value.match(/^[\d\.]+px$/);
},
transformer: function(prop) {
return prop.value.replace(/px$/, 'pt');
}
});
...// === CUSTOM TRANSFORM GROUPS ===StyleDictionaryPackage.registerTransformGroup({
name: 'styleguide',
transforms: ["attribute/cti", "name/cti/kebab", "size/px", "color/css"]
});
StyleDictionaryPackage.registerTransformGroup({
name: 'tokens-js',
transforms: ["name/cti/constant", "size/px", "color/hex"]
});
StyleDictionaryPackage.registerTransformGroup({
name: 'tokens-scss',
transforms: [ "name/cti/kebab", "time/seconds", "size/px", "color/css" ]
});
StyleDictionaryPackage.registerTransformGroup({
name: 'tokens-ios',
transforms: [ "attribute/cti", "name/cti/camel", "size/pxToPt"]
});
StyleDictionaryPackage.registerTransformGroup({
name: 'tokens-android',
transforms: [ "attribute/cti", "name/cti/camel", "size/pxToDp"]
});
// === CUSTOM TEMPLATES ===StyleDictionaryPackage.registerTemplate({
name: 'ios/generic.plist',
template: __dirname + '/templates/ios-generic.template'
});
...StyleDictionaryPackage.registerTemplate({
name: 'android/generic.xml',
template: __dirname + '/templates/android-generic.template'
});
...

Essentially, what is happening here is that — instead of using the formats and transforms offered out-of-the-box by Style Dictionary — I am creating custom functions (each one of which consists of just a few lines of code, and are all quite very similar). The names of these functions are then the parameters that are used to declare the formats, transforms, transform groups, and templates in the configuration of the build process. Simple as that!

The great thing about this is that everything is just plain JavaScript, and the APIs are well documented, so even a non-developer like me can create a custom build with a few lines of code (for me, this was a game changer).

If you want to see the entire build.js file in the demo project, and how the different parts connect one with the other, here is the link: https://github.com/didoo/style-dictionary-demo/blob/master/build.js

A few interesting tips & tricks that I’ve found out in the process, in case you want to do something similar for your build script:

  • you can override the names of the pre-defined functions (eg. you can register a size/px or a time/seconds function even if already exists)
  • you can use the prefix property to prepend a string to the tokens names
  • to get the list of pre-defined transform values in the transform groups (e.g. [‘attribute/cti’, ‘name/cti/pascal’, ‘size/rem’, ‘color/hex’] for the ‘JS’ group) you can do console.log(StyleDictionary.transformGroup[‘js’]) in your script and read the output in the command line shell
  • you can add custom meta-information to the tokens, and use it later in your filter/matcher functions and in the logic of your templates
  • you can add a filter declaration to a file block, to include only certain tokens values in the generated output files, based on some of their props (useful if you want to split the tokens into different files).

As you can see from the example above, using the registerTemplate function I have registered custom templates to generate PLIST and XML files in output for iOS and Android. The reason for that is that I wanted to have full control over the format of the generated files for these platforms, to meet the technical requirement that the native developers had, in order to adopt and consume the design tokens in their codebases.

If you want to do the same, and create your own templates, keep in mind that Style Dictionary uses the not-so-common lodash function _.template() to “compile” the templates. You will find almost nothing about this templating engine, apart from the official lodash documentation. The engine itself is pretty basic, but you can rely on Node/JavaScript to manipulate/format the values and the output. So my suggestion is to read carefully the examples section in the documentation of that function, where you can see what it can do, and infer what it can’t do, compared to other templating engines (remember that, according to the documentation, if you want you can use different templating languages, like Handlebars or similar: it’s up to you).

Beware: I’ll stress it again, because I want to be clear on this. What I have done is not what I suggest you to do, unless you have a very clear idea of what you need and what you want to achieve. Style Dictionary comes with a lot of pre-defined defaults, that most probably will work for you. Before starting to dig into all the possible customisations that you can have, try the default settings offered by the library, look at the output files, and see if they can suit your needs. Probably they will do. If they don’t, think how you want the output files generated, and see which one of the API methods you can use for that specific scope.

The generated design token files

We have seen how to set up a project with Style Dictionary and how to run a custom build. But what are the final results? How do the generated design token files look like? Well, here they are.

This is the output as Scss file for the web platform:

// ./dist/web/brand#1/tokens.scss$token-alias-color-brand-primary: #3B5998;
$token-alias-color-brand-secondary: #4267B2;
$token-avatar-size-xxsm: 36px;
...

As explained above, the variable names have a “token” prefix, that we use to differentiate the Scss variables that come from the design tokens from the normal variables declared in the Scss files.

This the ES6 JavaScript format file:

// ./dist/web/brand#1/tokens.es6.jsexport const TOKEN_ALIAS_COLOR_BRAND_PRIMARY = '#3B5998';
export const TOKEN_ALIAS_COLOR_BRAND_SECONDARY = '#4267B2';
export const TOKEN_AVATAR_SIZE_XXSM = '36px';
...

And this the flat JSON file:

// ./dist/web/brand#1/tokens.json[
{
"value": "#3B5998",
"type": "color",
"comment": "this is a comment",
"original": {
"value": "#3B5998",
"type": "color",
"comment": "this is a comment"
},
"name": "token-alias-color-brand-primary",
"attributes": {
"category": "alias",
"type": "color",
"item": "brand",
"subitem": "primary"
},
"path": [
"alias",
"color",
"brand",
"primary"
]
},
{
"value": "#4267B2",
"type": "color",
"comment": "this one is a comment too",
"original": {
"value": "#4267B2",
"type": "color",
"comment": "this one is a comment too"
},
"name": "token-alias-color-brand-secondary",
"attributes": {
"category": "alias",
"type": "color",
"item": "brand",
"subitem": "secondary"
},
"path": [
"alias",
"color",
"brand",
"secondary"
]
},
{
"value": "36px",
"original": {
"value": "36px"
},
"name": "token-avatar-size-xxsm",
"attributes": {
"category": "avatar",
"type": "size",
"item": "xxsm"
},
"path": [
"avatar",
"size",
"xxsm"
]
},
...
]

As you can see, the flat JSON file exposes all the extra properties and meta-informations attached to the design token properties, and makes the design tokens ready to be further consumed/processed by other tools (eg. a style guide, like in our case) in a very simple and straightforward way.

Similarly, for the native platforms, this is the output format for iOS:

// ./dist/ios/brand#1/tokens-all.plist<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>tokenAliasColorBrandPrimary</key><string>#3B5998</string>
<key>tokenAliasColorBrandSecondary</key><string>#4267B2</string>
<key>tokenAvatarSizeXxsm</key><string>36pt</string>
...
</dict>

And this is the output for Android:

// ./dist/android/brand#1/tokens-all.xml<?xml version="1.0" encoding="UTF-8"?>
<resources>
<color name="token_alias_color_brand_primary">#3B5998</color>
<color name="token_alias_color_brand_secondary">#4267B2</color>
<item name="token_avatar_size_xxsm" type="dimen">36dp</item> ...
</resources>

As you can see, for the native platforms I have converted the px values to pt/dp, while I have left the color values expressed in hex format. This is because both our iOS and Android application projects have already in place custom functions that can read hex values from PLIST and XML “style” files. Of course, this format is very specific to us, and you may need completely different formats. Before starting, I suggest you to speak with your iOS/Android developers and agree on which one is the best format for them (this may require the creation of custom formats/transforms/templates, as discussed above).

The example project on Github

As I have already mentioned, I have created a repository with an example very similar to the way I have set up the design tokens for our Design System at Badoo. You can see the demo at this GitHub address:

http://github.com/didoo/style-dictionary-demo

Here is a quick overview of the project structure:

  • the /src folder contains the JSON files used in input, each one of them containing the key/value/attributes tokens declarations
  • the /dist folder contains the generated files, in different formats for the different target platforms
  • the /templates folder contains the template files used to generate the files
  • the /build.js contains the entire build task, from the declarations of the custom functions, to the build configuration, to the actual build runner.

It’s a relatively simple use case, but shows — quite clearly, I hope — how to set up and configure a Style Dictionary project that can handle multi-brand multi-platform design tokens. As I said, feel free to use it for inspiration, or as a starting point for your own implementation.

Conclusions/Final thoughts

I have already said a lot, so I’ll try to be concise at least in the conclusions.

In the last days, weeks, I have used extensively Style Dictionary, and every time I find myself thinking Wow. It just works!. Everything in this project works as one would expect to, everything is so well-thought and clear (credits to Danny Banks for this) that the learning curve is almost zero. In no time you’ll find yourself doing things that you would have only dreamed of.

The best description of this library can be found in the contributing.md file:

The spirit of this framework is to allow flexibility and modularity so that anyone can fit it to their needs. This is why you can write your own transforms, formats, and templates with the register methods.

And I think flexibility is the keyword here. It’s no coincidence that one of the comments about Style Dictionary that I’ve read in the Design Systems Slack channel, was this one:

“I guess what I am seeing so far is it is ridiculously flexible” — Mark Johnston

So, if you had any doubts about starting to use design tokens in your system, now you have no more excuses: Style Dictionary is the perfect solution.

THE END.

Update:

I have written a follow-up post, about how in Cosmos we use design tokens beyond colors, typography & spacing, how we add meta-information to filter/group/post-process them, how powerful they are to describe component properties and specifications:

--

--

Cristiano Rastelli
Cristiano Rastelli

Written by Cristiano Rastelli

Design System Lead at HashiCorp. In love with Atomic Design, Design Systems & CSS architecture. Attendee/speaker/organiser of tech conferences and meetups.

Responses (6)