Designing Tokens — What Makes Great Design Tokens, and How to Build Them (Part 2)
This is a series of article where I present what I believe is a great way to consume and maintain design tokens. Examples are written in React and styled-components but the ideas in this article could be adapted to any frameworks.
Introduction
In Part 1 of this series, we came up with a set of desired interfaces for consuming design tokens. In this article, we implement the tokens to fit those interfaces. In Part 3, we will look at how to make the tokens more maintainable.
The Process
There will not be a lot of reading/reasoning in this article. I will follow through each iteration, as defined in the last article, and show you what the design tokens might look like at each stage. To see the reasoning behind each iteration please see Part 1.
We start by assuming that our tokens are initially defined like so:
const tokens = {
orange: "#ff9b00",
purple: "#5f259f",
blue: "#0096e6",
red: "#ff0000",
white: "#ffffff",
lighter: "#e8e8e8",
lightest: "#b2b2b2",
dark: "#999999",
darker: "#666666",
darkest: "#333333",
black: "#000000",
};
Iteration 1 — Grouping color by usage
In this iteration, we want to group the tokens together by type; whether they are text color or background colors or icon color.
Desired interface:
const MyBanner = styled.div`
color: ${tokens.text.darkGray}; //color: #333333
background-color: ${tokens.background.purple};
`;
New token definition:
const tokens = {
text: {
orange: "#ff9b00",
purple: "#5f259f",
blue: "#0096e6",
red: "#ff0000",
white: "#ffffff",
grayDarkest: "#333333",
},
background: {
orange: "#ff9b00",
purple: "#5f259f",
blue: "#0096e6",
white: "#ffffff",
grayLightest: "#b2b2b2",
black: "#000000",
},
icon: {
white: "#ffffff",
grayDark: "#999999",
},
};
Iteration 2 — Grouping tokens by type
Next, we introduce other token categories such as sizes (font sizes, line heights)and layouts (media queries, z-indices).
Desired interface:
const MyBanner = styled.div`
color: ${tokens.color.text.darkGray}; //color: #333333
background-color: ${tokens.color.background.purple};
font-size: ${tokens.size.font.sm1};
line-height: ${tokens.size.lineHeight.small};
@media (min-width: ${tokens.layout.media.tablet}) {
font-size: ${tokens.size.font.lg1};
z-index: ${tokens.layout.zIndex.bottomlessPit};
}
`;
New Token Definition:
const tokens = {
color: {
text: {
orange: "#ff9b00",
purple: "#5f259f",
...
},
background: {
orange: "#ff9b00",
purple: "#5f259f",
...
},
icon: {
orange: "#ff9b00",
purple: "#5f259f",
...
},
},
layout: {
media: {
tablet: "640px",
...
},
zIndex: {
bottomlessPit: "-9999",
...
},
},
size: {
border: {
small: "solid 1px",
...
},
font: {
sm1: "12px",
...
},
lineHeight: {
small: "1",
...
},
radius: {
small: "4px",
...
},
},
};
Iteration 3 — Merging token key with token value
In this iteration, we want to simplify the interface by including CSS property name as part of the tokens.
Desired interface:
const {color, size, layout} = tokens;
const MyBanner = styled.div`
${color.text.darkGray}; //color: #333333
${color.background.purple};
${size.font.sm1};
${size.lineHeight.small};
${layout.media.tablet} {
${size.font.lg1};
${layout.zIndex.bottomlessPit};
}
`;
New tokens:
const tokens = {
color: {
text: {
orange: "color: #ff9b00",
purple: "color: #5f259f",
...
},
background: {
orange: "background-color: #ff9b00",
purple: "background-color: #5f259f",
...
},
},
layout: {
media: {
tablet: "@media (min-width: 640px)",
...
},
zIndex: {
bottomlessPit: "z-index: -9999",
...
},
},
size: {
border: {
small: "border: solid 1px",
...
},
font: {
sm1: "font-size: 12px",
...
},
lineHeight: {
small: "line-height: 1",
...
},
radius: {
small: "border-radius: 4px",
...
},
}
Iteration 4 — Naming colors by functionality
Next, we want to name colors not by their color names, but according to the purpose it serves on the page.
Desired interface:
const MyBanner = styled.div`
${color.text.alt}; //color: #999999 OR color: #FFC0CB
${color.background.main1};
${color.border.alt};
`;
New Tokens:
const tokens = {
color: {
text: {
brand1: "color: #ff9b00",
brand2: "color: #5f259f",
interaction: "color: #0096e6",
error: "color: #ff0000",
inverse: "color: #ffffff",
default: "color: #666666",
alt: "color: #333333",
},
background: {
brand1: "background-color: #ff9b00",
brand2: "background-color: #5f259f",
interaction: "background-color: #0096e6",
default: "background-color: #ffffff",
alt: "background-color: #b2b2b2",
inverse: "background-color: #000000",
},
},
...
}
Iteration 5 — Intellisense
With Iteration 5, we want intellisense to give us more information about the tokens we are using. The interface itself does not change, but we need to add comments into the token definitions:
New Tokens:
const tokens = {
color: {
text: {
/** Orange #ff9b0 */
brand1: "color: #ff9b00",
/** Purple #5f259f */
brand2: "color: #5f259f",
/** Blue #0096e6 */
interaction: "color: #0096e6",
...
},
layout: {
media: {
/** min-width: 640px */
tablet: "@media (min-width: 640px)",
/** min-width: 940px */
desktop: "@media (min-width: 940px)",
/** min-width: 1200px */
largeDesktop: "@media (min-width: 1200px)",
},
zIndex: {
/** -9999 */
bottomlessPit: "z-index: -9999",
/** 1*/
default: "z-index: 1",
/** 800 */
overlay: "z-index: 800",
/** 9999 */
overTheMoon: "z-index: 9999",
},
},
...
};
You can already see that this is getting quite complicated. We will talk about how to simplify things so they are more maintainable in Part 3. For now, lets keep going.
Iteration 6 — Expose token name as Typescript interface
Lastly, we want to expose typescript interfaces with the keys of the tokens. To do this, we would write something like:
export interface ColorTextKey = "default" | "inverse" | "error"
export interface BgTextKey = "defualt | "inverse" | "alt"
...
Conclusion
Here’s what our final tokens looks like:
const tokens = {
color: {
text: {
/** Orange #ff9b0 */
brand1: "color: #ff9b00",
/** Purple #5f259f */
brand2: "color: #5f259f",
/** Blue #0096e6 */
interaction: "color: #0096e6",
/** Red #ff0000 */
error: "color: #ff0000",
/** White #ffffff */
inverse: "color: #ffffff",
/** Darkest Gray #333333 */
default: "color: #333333",
},
background: {
/** Orange #ff9b00 */
brand1: "background-color: #ff9b00",
/** Purple #5f259f */
brand2: "background-color: #5f259f",
/** Blue #0096e6 */
interaction: "background-color: #0096e6",
/** White #ffffff */
default: "background-color: #ffffff",
/** Gray #b2b2b2 */
alt: "background-color: #b2b2b2",
/** Black #000000 */
inverse: "background-color: #000000",
},
icon: {
/** Black #ffffff */
white: "color: #ffffff",
/** Black #999999 */
grayDark: "color: #999999",
/** Blue #0096e6 */
interaction: "color: #0096e6",
},
},
layout: {
media: {
/** min-width: 640px */
tablet: "@media (min-width: 640px)",
/** min-width: 940px */
desktop: "@media (min-width: 940px)",
/** min-width: 1200px */
largeDesktop: "@media (min-width: 1200px)",
},
zIndex: {
/** -9999 */
bottomlessPit: "z-index: -9999",
/** 200 */
dropdown: "z-index: 200",
/** 400 */
sticky: "z-index: 400",
/** 600 */
popover: "z-index: 600",
/** 1*/
default: "z-index: 1",
/** 800 */
overlay: "z-index: 800",
/** 1000 */
modal: "z-index: 1000",
/** 1200 */
snackbar: "z-index: 1200",
/** 1400 */
spinner: "z-index: 1400",
/** 9999 */
overTheMoon: "z-index: 9999",
},
size: {
font: {
/** 12px - Small fonts starts at 12px and increments by 2 */
sm1: "font-size: 12px",
/** 14px - Small fonts starts at 12px and increments by 2 */
sm2: "font-size: 14px",
/** 16px - Small fonts starts at 12px and increments by 2 */
sm3: "font-size: 16px",
/** 18px - Small fonts starts at 12px and increments by 2 */
sm4: "font-size: 18px",
/** 24px - Large fonts starts at 24px and increments by 8 */
lg1: "font-size: 24px",
/** 32px - Large fonts starts at 24px and increments by 8 */
lg2: "font-size: 32px",
/** 40px - Large fonts starts at 24px and increments by 8 */
lg3: "font-size: 40px",
/** 48px - Large fonts starts at 24px and increments by 8 */
lg4: "font-size: 48px",
},
}
},
};type ColorBackgroundKeys = keyof typeof tokens.color.background
type ColorTextKeys = keyof typeof tokens.color.text
type ColorIconKeys = keyof typeof tokens.color.icon
type LayoutMediaKeys = keyof typeof tokens.layout.media
type LayoutZIndexKeys = keyof typeof tokens.layout.zIndex
type SizeFontKeys = keyof typeof tokens.size.font
This works, and it satisfies all the requirements we came up with in Part 1. We’ll refer to this structure as the Consumption-friendly-format from now.
However, it is not perfect. Not only does it looks intimidating and could be a bit difficult to maintain. In Part 3, we’ll look at some of the issues the Consumption-friendly-format have and introduce the Maintenance-friendly-format.