4. Advanced

📖 Articles about advanced token topics

You made it! The final chapter of design tokens! Here we will look at some more advanced topics on design tokens.


4.1 Design token graphics

One interesting thing you can do with design tokens is use them in SVGs to either generate static PNGs or other SVG files. For example, lets take a look at the logo for this demo design system in SVG:

If we wanted the color of our logo to be defined by a design token that defines our primary brand color...

To me, this is one of the cooler things you can do with design tokens. You can generate static assets like images! Let's do that with the logo for our fake design system that I tried to make look like the W3C Design Token logo. First create an assets/ directory in the root of our project and add a new file called logo.svg in there with this code:

<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M89.75 49.847L115 35.1493V93.9403L64.5 123.336V90.9254" stroke="#cccccc" stroke-width="8" stroke-linejoin="round"/>
<path d="M39.25 49.847L14 35.1493V93.9403L64.5 123.336V90.9254" stroke="#cccccc" stroke-width="8" stroke-linejoin="round"/>
<path d="M14 34.3955L64.5 5L115 34.3955" stroke="#cccccc" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="64.0426" cy="64.4623" r="26.0255" stroke="#cccccc" stroke-width="8" stroke-linejoin="round"/>
</svg>

Then lets add a new type of token to our tokens, create an assets directory in tokens/ and create a file called svg.json in there. The SVG file we just created is the value of the design token we are about to create.

{
"asset": {
"svg": {
"logo": {
"value": "assets/logo.svg",
"attributes": {
"width": 128,
"height": 128
}
}
}
}
}

The value is the file path to the actual SVG file. We can give it additional attributes like its width and height which we can use later. Right now this won't actually do anything. Let's change that.

Because this is a special type of token, one which we want to generate whole files based on, we are going to need to use actions. Actions are basically an escape hatch to let us really do anything we need from our design tokens. You can move files around, generate files, really do anything based on your design tokens. Let's create a custom action. Create an actions/ directory and a file called generateSVG.js in there.

To generate these files we will first need to install some dependencies:

npm install fs-extra lodash svg2vectordrawable sharp --save-dev

Now open actions/generateSVG.js. An action in style dictionary has do and undo functions that take the transformed dictionary and the platform configuration as arguments. Let's set up the action to just see how actions work:

module.exports = {
do: (dictionary, config) => {
console.log(config);
},
undo: (dictionary, config) => {
}
}

Now to use this action, we can add it to an action object in our config:

action: {
generateSVG: require('./actions/generateSVG'),
},

Then let's create a new platform to run this action. Create a new platform named "asset", give it a transformGroup of "assets", and add actions as an array with 'generateSVG' in there:

asset: {
transformGroup: `assets`,
actions: [`generateSVG`]
},

Now if we run npm run build we should see when the asset platform runs it outputs the platform configuration to the console.

Now the cool thing is we can use design tokens inside this SVG and generate PNGs or other assets from it! 🤯

Open up assets/logo.svg and replace all the stroke colors with: <%= color.brand.primary.value %>

<svg width="128" height="128" viewBox="0 0 128 128" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M89.75 49.847L115 35.1493V93.9403L64.5 123.336V90.9254" stroke="<%= color.brand.primary.value %>" stroke-width="8" stroke-linejoin="round"/>
<path d="M39.25 49.847L14 35.1493V93.9403L64.5 123.336V90.9254" stroke="<%= color.brand.primary.value %>" stroke-width="8" stroke-linejoin="round"/>
<path d="M14 34.3955L64.5 5L115 34.3955" stroke="<%= color.brand.primary.value %>" stroke-width="8" stroke-linecap="round" stroke-linejoin="round"/>
<circle cx="64.0426" cy="64.4623" r="26.0255" stroke="<%= color.brand.primary.value %>" stroke-width="8" stroke-linejoin="round"/>
</svg>

Now open up actions/generateSVG.js. First thing we are going to do is grab all the svg asset tokens. In the body of the do method add this:

const svgs = dictionary.allProperties.filter(token => {
return token.attributes.category === `asset` &&
token.attributes.type === `svg`
});

We are going to use lodash's template to replace the design tokens in the SVG with their value. Import lodash/template and because Node's file system module sucks, import 'fs-extra' at the top of the file:

const fs = require('fs-extra');
const template = require('lodash/template');

Now let's iterate over our svg tokens and

  1. Create a lodash template from the svg file
  2. Run that function passing in the dictionary object which has all the transformed tokens
  3. Create a file path based on our platform config
  4. Ensure that file and directory exist, if not it will create the directories
  5. Finally write the output to the output path
svgs.forEach(token => {
const src = template( fs.readFileSync(token.value) );
const output = src(dictionary.properties);
const outputPath = `${config.webPath||''}${token.value}`;
fs.ensureFileSync(outputPath);
fs.writeFileSync(outputPath, output);
console.log(`✔︎ ${outputPath}`);
});

Save that. Now we need to add the webPath on our platform config in sd.config.js

asset: {
transformGroup: `assets`,
webPath: `web/dist/`,
actions: [`generateSVG`]
},

Run npm run build and you should see a newly generated SVG file at web/dist/assets/svg/logo.svg and the stroke colors should be hex codes now!

Let's use this SVG file in our web demo.

Open web/demo/src/components/Logo/Logo.js and import the newly created SVG file:

import React from 'react'
// You could also just import this normally and use it as an image src
import logo from '!!raw-loader!clarity-design-tokens/web/dist/assets/logo.svg'
const Logo = () => (
<span className="logo" dangerouslySetInnerHTML={{__html: logo}} />
)
export default Logo

Android

Now lets build an Android vector based on this SVG. Luckily for us, there is a nifty NPM package called svg2vectordrawable that turns SVGs into Android Vector Drawables which we installed earlier.

First let's add androidPath to our asset platform configuration and give it a value of "android/claritydesigntokens/src/main/res/drawable/". Now it should look like this:

asset: {
transformGroup: `assets`,
webPath: `web/dist/`,
androidPath: `android/claritydesigntokens/src/main/res/drawable/`,
actions: [`generateSVG`]
},

Then back in actions/generateSVG.js import the svg2vectordrawable library:

const s2v = require('svg2vectordrawable');

Right below fs.writeFileSync lets add code to build our Android vector drawable.

const androidPath = `${config.androidPath}${token.name}.xml`;
fs.ensureFileSync(androidPath);
s2v(output).then(xml => {
setTimeout(() => null, 0); // forces node to not exit immediately
fs.writeFileSync(androidPath, xml);
console.log(`✔︎ ${androidPath}`);
});

Run npm run build again and see the new file it created!

Now let's integrate it into our Android demo app. Again, because of Android's merging of resources, our logo actually isn't showing up yet because or demo app also defines a drawable named "logo". Go ahead and delete logo.xml in android/demo/src/main/res/drawable/. Rebuild the app and see the new logo in color!

iOS

Last but not least, we have iOS. Images in iOS have an "imageset" that is placed in an asset directory. The "imageset" contains multiple resolutions of the image and a JSON file that tells iOS where to get the images.

In order for CocoaPods to include our assets we need to update our podspec file:

spec.resources = "ios/dist/DesignTokens.xcassets"

Then let's add the code to generate an imageset in actions/generateSVG.js

const resolutions = [1,2,3];
const iosPath = `${config.iosPath}${token.name}.imageset/`;
fs.ensureDirSync(iosPath);
const contents = {
images: [],
"info" : {
"author" : "xcode",
"version" : 1
}
}
resolutions.forEach(resolution => {
const fileName = `${resolution}x.png`;
contents.images.push({
filename: fileName,
idiom: "universal",
scale: `${resolution}x`
});
sharp(Buffer.from(output, `utf8`))
.resize({ width: resolution * token.attributes.width })
.png({ compressionLevel: 0 })
.toFile(`${iosPath}${fileName}`)
.then(info => {
setTimeout(() => null, 0); // forces node to not exit immediately
});
});
fs.writeFileSync(`${iosPath}Contents.json`, JSON.stringify(contents, null, 2));

Add an iosPath to our asset platform configuration with a value of "ios/dist/DesignTokens.xcassets/".

Now run npm run build and check out the new imageset for iOS!

Because we added new files, we need to close Xcode and re-install our cocoapod. Go into the ios/demo directory and run pod install.

Now open up our Xcode workspace and lets add the image to our home screen. Open HomeView.swift and update image to add a bundle identifier:

// "logo" is the name of the image in the asset catalog, which is the
// directory name minus ".imageset"
Image("logo", bundle: Bundle(identifier: "org.cocoapods.ClarityDesignTokens"))

Rebuild the iOS app and now the logo should be the brand color!

There is a way to get the bundle based on a class in that bundle, but I couldn't figure it out. Using the bundle identifier is easy enough because the name of the bundle is "org.cocoapods." + the name of the pod.

In the newer iOS SDK that includes SF Icons, there is a way to use SVGs as images so we don't need to export PNG files. But I didn't have time to figure that out... in theory because it is just an SVG we should be able to export to an SF icon SVG file.

At the end of this step you should have an asset platform in your sd.config.js file that looks like this:

asset: {
transformGroup: `assets`,
webPath: `web/dist/`,
androidPath: `android/claritydesigntokens/src/main/res/drawable/`,
iosPath: `ios/dist/DesignTokens.xcassets/`,
actions: [`generateSVG`]
},

And actions/generateSVG.js that looks like this:

const fs = require('fs-extra');
const template = require('lodash/template');
const s2v = require('svg2vectordrawable');
const sharp = require('sharp');
module.exports = {
do: (dictionary, config) => {
const svgs = dictionary.allProperties.filter(token => {
return token.attributes.category === `asset` &&
token.attributes.type === `svg`
});
svgs.forEach(token => {
const src = template( fs.readFileSync(token.value) );
const output = src(dictionary.properties);
const outputPath = `${config.buildPath||''}${token.value}`;
fs.ensureFileSync(outputPath);
fs.writeFileSync(outputPath, output);
const androidPath = `${config.androidPath}${token.name}.xml`;
fs.ensureFileSync(androidPath);
s2v(output).then(xml => {
setTimeout(() => null, 0); // forces node to not exit immediately
fs.writeFileSync(androidPath, xml);
console.log(`✔︎ ${androidPath}`);
});
const resolutions = [1,2,3];
const iosPath = `${config.iosPath}${token.name}.imageset/`;
fs.ensureDirSync(iosPath);
const contents = {
images: [],
"info" : {
"author" : "xcode",
"version" : 1
}
}
resolutions.forEach(resolution => {
const fileName = `${resolution}x.png`;
contents.images.push({
filename: fileName,
idiom: "universal",
scale: `${resolution}x`
});
sharp(Buffer.from(output, `utf8`))
.resize({ width: resolution * token.attributes.width })
.png({ compressionLevel: 0 })
.toFile(`${iosPath}${fileName}`)
.then(info => {
setTimeout(() => null, 0); // forces node to not exit immediately
});
});
fs.writeFileSync(`${iosPath}Contents.json`, JSON.stringify(contents, null, 2));
});
},
undo: (dictionary) => {
}
}

4.2 Theming, extending, and overriding

I know this is a common refrain by now, but there are many ways you can accomplish theming in your design token setup. It is helpful to think about what you are trying to accomplish with theming because it can mean different things to different people. Say you work in a company that has multiple brands or products, each with their own style or theme, but you want to share tokens across them all.

Overriding

This is similar to theming, but we can do it per platform as well. For example you might want to have different font sizes for Android, or different paddings for mobile (Android and iOS) than web. In a similar way to theming, we can override tokens based on platform. Again this technique comes from Cristiano Rastelli's article

const platforms = ['web','android','ios'];
const brands = ['brand1','brand2','brand3'];
platforms.forEach(platform => {
brands.forEach(brand => {
StyleDictionary.extend({
source: [
`tokens/core/**/*.json`,
`tokens/brand/${brand}/**/*.json`,
`tokens/platform/${platform}/**/*.json`
]
})
})
});

4.3 Dynamically generated tokens

Way back in the first section I made a note that design tokens could be built in a programming language like Typescript or Javascript? Well now is our time to look at how that can work!

Token files in Style Dictionary can be any file that require() in Node can understand: JSON, JSON5 (we added the JSON5 library), and Node modules! As long as you export a static object, you can use Node modules to store your design tokens.

Note: I don't think there is a way to do this in Theo without generating a JSON file and referencing it in Theo

Let's generate our core color palette from a simple set of colors.

Most of the time we think of and store colors in terms of hex values, which I think is not optimal. I don't know about you, but I cannot read hex codes. I understand how to read them, but give me a hex code and unless it is a shade of pure grey, I am lost.

How to read hex codes
Hex codes are either 6 or 8 digits long (CSS allows short-hand hex codes like 3 digits, but don't worry about that) and those digits represent the red, green, blue, and alpha channels. A 6-digit hex only has the red, green, and blue. Each channel has 2 digits, so the first 2 numbers of a hex code in CSS is the value of the red channel. Android puts the alpha channel first, which is backwards compared to CSS. Hexadecimal is a base-16 number system, so instead of 0-9, numbers can go 0-16. So there are 6 extra places for each digit, represented by the letters a-f. The 2 digits of each channel go from 0-256 (16 * 16).

https://github.com/amzn/style-dictionary/blob/main/examples/advanced/node-modules-as-config-and-properties/properties/color/core.js

const Color = require('tinycolor2');
// If you wanted to, you could generate the color ramp with base colors.
// You get less control over specific color values,
// but it might be more clean to do it this way.
const baseColors = {
red: {h: 4, s: 62, v: 90},
purple: {h: 262, s: 47, v: 65},
blue: {h: 206, s: 70, v: 85},
teal: {h: 178, s: 75, v: 80},
green: {h: 119, s: 47, v: 73},
yellow: {h: 45, s: 70, v: 95},
orange: {h: 28, s: 76, v: 98},
grey: {h: 240, s: 14, v: 35},
}
// Use a reduce function to take the array of keys in baseColor
// and map them to an object with the same keys.
module.exports = Object.keys(baseColors).reduce((ret, color) => {
return Object.assign({}, ret, {
[color]: {
"20": { value: Color(baseColors[color]).lighten(30).toString()},
"40": { value: Color(baseColors[color]).lighten(25).toString()},
"60": { value: Color(baseColors[color]).lighten(20).toString()},
"80": { value: Color(baseColors[color]).lighten(10).toString()},
"100": { value: baseColors[color]},
"120": { value: Color(baseColors[color]).darken(10).toString()},
"140": { value: Color(baseColors[color]).darken(20).toString()}
}
})
}, {});

One additional benefit of using Node modules to define your design tokens is you can get rid of having to write that pesky object structure for every JSON file and instead require and export objects in Node:

module.exports = {
color: require('./color'),
size: require('./size')
}

4.4 Documenting

I could probably spend multiple hours talking about documenting design tokens. If you want some inspiration for what design token documentation can look like in a design systems website, I wrote an article for that. Although in that article I didn't really go into how to get design tokens into your documentation site. So let's do that now.

Add metadata to tokens that then get displayed in documentation. For example

{
"color": {
"background": {
"primary": {
"value": "{color.core.grey.0.value}",
"documentation": {
"description": "This color is used as the default background to all pages."
}
}
}
}
}

You can access this metadata in your documentation site. Let's see how to do that.

First, lets add a new file in our web platform in our sd.config.js file:

format: `json`,
destination: `tokens.json`

This will output our design token object with a lot of extra metadata. Run npm run build and take a look at the tokens.json file it created. If you find the color.background.primary token you should see our documentation in there. We can now use this information in our docs site.

Open up web/demo/src/pages/tokens/color.mdx

Import this new file:

import ClarityDesignTokens from 'clarity-design-tokens/web/dist/tokens.json'

And I already built a few documentation components to help out. Import {CoreColors} from $components/Color

Now in the core colors section of our page add

<CoreColors {...ClarityDesignTokens.color.core.neutral} />

Neat!

Here is a super cool technique that I didn't have time to fit into this workshop to get all the variable names of a token on different platforms: https://github.com/sproutsocial/seeds-packets/blob/develop/packets/seeds-utils/style-dictionary/index.js


4.5 Design token CMSes

Do design tokens need to be in code? Technically no.

While experiments like these are interesting, I recommend keeping design tokens in code so that they are versioned in Git and follow the same reviewing and releasing mechanisms as any other code.

Design tokens in Airtable?

Design tokens in Google Sheets?

I took that as a challenge and wanted to do the same thing with Style Dictionary. So I did. Admittedly, storing design tokens in Google sheets works a bit better with Theo because Style Dictionary has no defined structure and can be an object with any number of levels.

While I think experiments like this are interesting and fun, the more technology we layer on it adds more complexity and technical constraints.

4.6 Tools, services, and other bits

I consider these separate from using a framework like Theo, Style Dictionary, or Diez because your design tokens are not in a code package in a repository, they live in the tool or service. I think there are some interesting things in each of these tools though.

Apologies if I miss any here. Let me know if I did and I can add it in


4.7 The future of design tokens

Is up to you! There are a lot of new tools, projects, articles, and talks about design tokens happening right now which is awesome.

W3C Design Token Community Group

If this is your jam and want to contribute to what the future of design tokens looks like, join the community group! Or be active on the Github as the group posts RFCs (request for comment) and drafts.


Ending slides

Thank you for joining me on this journey through the seas of design tokens. Hopefully this was fun and educational. If you have any questions about anything, or want to talk more about design tokens, reach out to me on Twitter and I'd love to help 😁