Design Tokens
My process in finding a solution to UX debt. What the Hex outlines token definitions.
Definition
Design tokens are design values used to create a design system. The result enables consistent, flexible, and harmonious designs leading to a great end experience and a souped-up creation process for designers and developers. Tokens become the source of truth for design decisions, reducing the number of design inconsistencies and assumptions developers make when interpreting designs. They also provide granular control of the UI at scale.
Here is my approach to architecting color design tokens for a Fortune 500 company.
Problem Statement
I began addressing solutions to UX debt in the project I acquired. The files had a variety of styles built up over the years. Each designer provided their own flare which contributed to inconsistencies across the app. As it grew, so did the UX debt. Among the discrepancies were roughly 30 different button styles, which I consolidated; however, correcting all debt was more than I could handle.
I discovered design tokens which were not only a promising way to reduce debt by having a source of truth for design decisions but a way to mitigate the possibility of debt in the future. Like creating a component in Figma, Styles would update across all instances, but this time with a fine-tooth comb. Designs could consistently be updated without having old styles stick around.
Why I Created My Own
I decided to create my own system of Tokens so that I would have the right amount of flexibility and control over my design decisions. I derived a lot of ideas and concepts from other systems though.
Color Pallet
I found it essential to have a robust color pallet that could be tokenized. I used http://Leonardo.io to create a pallet based on the brand color already used. I multiplied the colors by ten, making a scale of different levels of exposure. Covering all possible UI types color would be applied.
Color Categories
Adding a full range of colors would help provide different semantic meanings to the designs. Green means go, and red means stop. Each hue carries a semantic meaning. Whether we think about it or not, we choose all colors based on these inherent values and how they make us feel. For this reason, I chose a base, primary, semantic (success, warning, and danger), and extended colors for my pallet.
Considering Dark Mode
The result was a pretty hefty color pallet, but the intention was to trim the fat after identifying each color’s use case.
http://Leonardo.io made it easy to create a dark-mode color pallet. It adjusted my colors based on how dark I wanted the background to be. It took some time to readjust some of the colors to get it exactly how I wanted. Even though I use pure white, #FFFFFF, as my background color in light mode, I did not want to use pure black, #000000, in dark mode – it’s too sharp. I aimed to have my light mode neutral.1000 become my dark-mode background color which would enable a seamless and consistent switch from light mode to dark mode.
Example
a. Light Mode: Background = #FFFFFF
b. Dark Mode: Background = #0E0E0E
I needed the 500-level color (in the middle) to be the breaking point for using dark text on a light surface and light text on a dark surface. That way the text colors relative to their surface would stay accessible regardless of being in light or dark mode.
Accessibility
To test accessibility, I checked contrast levels using dark text (Neutral.1000) on colors 0-500. and light text (Neutral.0) as text on 600-1000. Checking the 500-level contrast was important because its the lowest level of contrast before the text color switched from dark to light.
Example (picture)
Neutral.1000 on Neutral.500
a. Light Mode: #E0E0E0 on #79859E
b. Dark Mode: #FFFFFF on #636C80
In light mode, all 500-level colors received the AA rating (4.5:1) for normal text. Ranging from 5.18:1- 5.23:1. In dark mode, all 500-level colors met the AA rating as well, ranging from 5.26:1 - 5.28:1.
Contrast ratios right where I want them. I knew switching between themes would work well, and I knew scaling into even more themes would work the same. These colors became my global set of tokens.
Global Tokens
I took these raw hex values and created my first layer of tokens. Raw values provide no information on how to apply the color. The lack of semantic meaning leaves the designer and developer guessing at the correct implementation. Matching the semantic meaning with the number on the color scale provided a name. This global identity creates the foundation for applying the colors to more specific areas in the design.
Example
a. #0070BA – A raw value with no defined purpose.
b. primary.600 – An identity is given to the raw value, providing more clarity.
Alias Tokens
To harness the true power of tokens, I needed to create Alias Tokens. Alias Tokens define a more specific spot to apply a style. My Global Tokens set would be referenced for the value. Two different Alias tokens can reference the same Global Token. The difference is in the role and application the Alias Token has. Depending on the creative direction of the designer, the architecture of the Alias Tokens can allow for more or less control of the UI.
Example
Global Token: primary.600
a. Alias Token: primary.background references primary.600 and is used for a background.
b. Alias Token: primary.border references primary.600 and is used for a border.
The true beauty of tokens comes out when any change needs to happen. If, for some reason, our primary color changes from blue to green, updating designs becomes easy. Once the raw value is changed, so do all of the referencing tokens. The Global Token and all Alias tokens change. if we only want a specific UI element to change, we can redirect an Alias Token to a different Global Value. This type of control is not available with just color styles in Figma.
Example
a. Global Token primary.600 changes raw value from #0070BA to #3CBA00. Alias Token primary.background and primary.border change to the new color.
b. Alias Token primary.background redirects its Global Token value from primary.600 to primary.500. Only the background color changes.
Knowing how to create a robust Alias Token structure was important. I looked at other systems to bring clarity to my implementation. The cloudy part for me was understanding how to find the right balance for grouping UI elements under one Alias Token. This group would have to all change color if I ever wanted to make an update. Giving every UI element a unique Alias Token would mean a ton of granular control, but not at scale. The opposite is also true. No granular control with large buckets, but it would all be done at scale.
Since names give identity, I started seeking an understanding of naming conventions, to hopefully discover the group structure.
ASANA gave a great talk on how they named tokens by going through a series of decisions. They considered the sentiment, usage, prominence, and interaction of the element they wanted to create a token for. A pressed button background would become primary.background.medium.default. In other words, what kind of color do we want? Where is that color being applied? Will the color be brighter or darker? And is there a state that can be applied to the color (ex. hover, pressed)?
Example
Prompt: I want to create a token for a button background.
a. Sentiment: Primary
b. Usage: Background
c. Prominence: Medium
d. State: Default
Name Result: primary.background.medium.default
With an exact formula for naming, creating each token was easy; however, using them became difficult. I might have a text named neutral.text, but if that text became an error state, the formula says to add that interaction to the end of the default name. This results in a conflicting name: neutral.text.error. The neutral sentiment means a gray color, and the error state means a red color.
A lesson I learned, later on, was that I needed to treat an Error state differently than a Disabled state, or a Pressed State. To the credit of ASANA, I believe this is what they do, but I misinterpreted their diagram and had a little detour in my discovery process.
Example
default text becoming an error state
a. neutral.text - The default state
b. neutral.text.error - Contains both the neutral and error sentiment.
I looked into other approaches to naming Alias tokens. Material Design offers a compelling structure. All UI elements are considered together. They look at the color and UI relationship. A light surface and light text would not work. For example, you have “Primary” and “On Primary” or “Background” and “On Background”. Knowing the surface color gives us insight into the appropriate text color.
Example
a. Primary - the surface color.
b. On-Primary - the text color on the surface color.
Carbon Design takes a similar approach. They use a layering method starting with the background up to surface-04. This approach allows for contrast amongst layers and content. If we want to apply a text to surface-02 we know to use the text-02 token because that contrast ratio will be right. This also offers more flexibility when creating multiple themes. A surface and background can both be white in light mode, but in dark mode, making those layers two different neutrals can be appropriate. This way we can use the token architecture to have the right amount of control at scale.
Example
a. Light mode: Surface-01 - White, Surface-02 - Gray 10, Surface-03 - White, Surface-04 - Gray
b. Dark mode: Surface-01 - Gray 100, Surface-02 - Gray 90, Surface-03- Gray 80, Surface-04 - Gray 70
Understanding these UI relationships and their naming conventions gave me crucial insight into how I would approach my Token architecture.
Component Tokens
Although I don’t currently use component tokens, they are worth mentioning. Component Tokens can be created for more specific UI areas. A button could have a component token for example, and that token could not be applied to a text field. This level is very specific, which makes it easy to understand how the token is used. For the button background, we could name the token: button.background.primary. As I started to create a custom solution for what I needed, being this explicit was not necessary. I can still glean all benefits of tokens at the Alias level.
Example
a. Global Token: Primary.600
b. Alias Token: primary.background
c. Component Token: button.background
My Alias Rules
I created a method to use these relationships and layering ideas in my approach. I categorized the anatomy of all UI to better understand the interconnected relationship. Any UI element can be put into one of three buckets. A surface, border, or type of content. A background or container would be considered a surface. Text, icons, and illustrations are content forms. While borders are simply, borders.
Standard Range
Using the predefined 0 to 1000 Global token scale, Alias tokens are assigned a subset range for each UI type. Surfaces will remain in the 0-100 range. Borders between 200-600. Content from 600-1000. These rules easily apply to 90% of use cases, but they will not work for all.
Example
a. 0-100: surfaces
b. 200-600: border
c. 600-1000: content
Inverse Range
In the unique case of a button, for example, the surface is darker than the 0-200 range. Here, an inverse token style is used. Carbon Design uses Inverse tokens, and it fits well with the solution I came up with. This rule set flips the normal rules. Content has a lower range and surfaces have a higher range. The layer association remains – only inverse content is used on inverse surfaces.
These rules apply regardless of the color sentiment. The system works because each color sentiment will have the same perceived brightness within their appropriate lanes. For example, a Neutral.100 will have the same brightness as a Primary.100 even though the hue changes. This way, regardless of sentiment, the system works. If we decide to change our primary color from blue to orange the UI should not break as long as the perceived brightness is the same. The same applies when swapping themes. All that is needed is a new scale of colors that can be swapped out for the original colors.
States
Different states rely on a similar formula. In the lesson I learned earlier, not all states are the same. Some are interactions with the semantic value and some are the semantic value.
Referencing Material Design, there are five different states to consider. Disabled, Focused, Enabled, Pressed, and Error. The rule I gave to disabled states was: regardless of the sentiment color used, the disabled version of that color would move one step down (ex. 600 to 500) and change to a neutral sentiment. (Note: I did not want to bother with opacity for disabled states since that causes more of a headache than it’s worth when it comes to development.) The Focused state would be one step down in the respective sentiment (I did not use the focus state for mobile). The enabled state remains the same as the default primary color. For simplicity, I cut out the enabled state because it was synonymous with the default state. I did not see the need for that level of granular control. The pressed state is one step up. And the error state is no longer a state but a sentiment.
Example
a. Disabled: Move one to the left, and use the neutral sentiment
b. Focused: Move one to the left in the same sentiment. (not used)
c. Enabled: No change. (not used)
d. Pressed: Move one to the right in the same sentiment
e. Error: Use danger sentiment and no lane change. (becomes a new token)
The inverse rule set keeps the same state patterns – they are not inverted. It would be weird for a pressed button to become brighter and not darker if those are the rules applied everywhere else.
Prominence
I differentiate between surface levels and prominence. If surface-01 can only go with content-01, prominence can be applied to surface-01 or surface-02. The prominence level is simply a higher or lower subcategory to the range already predefined. In the case of text, I use two prominences. A strong prominence was used for my H1 and H2 text, and a medium prominence was used for H3, H4, and H5. This provides creative control to applying a hierarchy within the same layer.
Naming Structure
My token naming structure follows this formula: ui.prominence.sentiment.state. We start from the birds-eye perspective and get more granular. Identifying the UI will consider the where and what of the token. It informs where on a scale of 0 to 1000 the token will be. The prominence is like an adjective to the UI, providing a better understanding of the brightness level the UI element will receive. A step or two up or down the scale. Once the exact spot on the scale is identified, we can add the sentiment. Lastly, the state comes at the end because any state will be an alteration to the default. (Remember, the error state is a misnomer since it belongs within the danger sentiment.)
Trimming the Fat
Since I now have neutral, primary, success, danger, warning, and 6 extended sentiments – all of which have 10 brightness levels, totaling 110 colors to choose from, I needed to trim out what won’t be used. I stress-tested the system to identify where colors were being used. If a global token wasn’t referenced, I removed it.The result was a robust architecture with just the right balance of control I was aiming for. Finally, the system would do all of the hard work for me.The tokens can now be used as a source of truth between the designer and developer. No more confusion.
Result
The Positives
An extremely robust set of tokens.
The contrast level remained the same regardless of surface level.
The global tokens could easily be swapped out to create new themes
Using the tokens was easy. Search the styles for surface, border, or content, then select the sentiment you want with the appropriate surface level.
Designers and developers have a source of truth. No more hex codes.
UX Debt was mitigated
The difficulties
Adoption of a new more complex system
A lot of rework to change color styles to color tokens.
Learning rules and how to implement them for other designers
Development implementation may be slightly different depending on the platform (iOS, Android)
Keep in Mind
The solution I came up with may not be the right solution for you. I would highly recommend continuously talking with developers as you form your tokens. Making a system that only works for one side undermines the intentions.
Next Case Study
What the Hex