Table of Contents
1 | Introduction to Design Tokens
2 | Managing and Exporting Design Tokens With Style Dictionary
3 | Exporting Design Tokens From Figma With Style Dictionary
4 | Consuming Design Tokens From Style Dictionary Across Platform-Specific Applications
5 | Generating Design Token Theme Shades With Style Dictionary
6 | Documenting Design Tokens With Docusaurus
7 | Integrating Design Tokens With Tailwind
8 | Transferring High Fidelity From a Design File to Style Dictionary
9 | Scoring Design Tokens Adoption With OCLIF and PostCSS
10 | Bootstrap UI Components With Design Tokens And Headless UI
11 | Linting Design Tokens With Stylelint
12 | Stitching Styles to a Headless UI Using Design Tokens and Twind
What You’re Getting Into
In a previous article, I made a case for building framework-specific UI component libraries that encapsulate common functionality but not design specifications (which I called headless UIs).
The idea is that you get a UI component library that is flexible to change styles over time without having to redo the implementation of common functionality. It also would tend to be less awkward than framework-agnostic solutions like web components.
Once a headless UI is built (see Tailwind Labs’ Headless UI as an example), you can create a wrapping library that stitches together the headless UI with styles from a design system.
To apply the design tokens to a headless UI, you can use Style Dictionary which offers the ability to transform the specifications of a design system, the design tokens, into platform deliverables such as CSS, SASS, or JS variables.
If you have a preference for using Tailwind, then you can integrate your design tokens with it.
Recently, I’ve made some experimental tooling to help with this process.
First, I’ve created an API that generates a Tailwind configuration file from a set of design tokens.
Second, I’ve created an
API that generates twind/style
instances from a set of design
tokens.
In this article, I’ll unpack more details about tooling to help facilitate ideas for others to follow suit.
Component Vs. Value Tokens
One of the challenges that I have found in researching and working with design tokens is that there is not yet a formal specification for how to name and categorize design tokens.
That day will come but for now, I will take a stab at a distinction between component tokens and value tokens.
“Component” tokens are design tokens that specify the design specifications of UI components. “Value” tokens specify general values. Component tokens will have the same value as a value token.
For example, there may be the following “component” tokens:
component.button.base.color = red
component.button.base.padding = 1rem
These component tokens would have the same value of “value” tokens:
component.button.base.color = red
component.button.base.padding = 1rem
+ color.primary = red
+ spacing.4 = 1 rem
In other words, “component” tokens are composed of “value” tokens and would be used to style UI components. However, value tokens may be used in other parts of an application on their own.
Tooling fo Value Tokens
Making a distinction in your design tokens between component and value tokens allows for the ability to make unique tools.
A potential tool for value tokens is to make them accessible via utility classes by using a tool like Tailwind.
My idea was to create a function that would take in an official set of design tokens and return a Tailwind configuration file.
For this to work, there are several steps in the logic.
First, the tokens have to be validated to match an expected format. In my case, I am expecting a flat JavaScript object or an aggregation of flat key-value modules:
export const ColorPrimary = "red";
export const Spacing1 = "0.25rem";
Second, it ignores component tokens as this tool is to expose the value tokens.
Third, it iterates through each design token and matches it against Tailwind’s configuration/plugin types.
Once the configuration key is found, the only remaining value to access is the
value token’s value. This is found by removing the part of the token key that
corresponds to a type (i.e. color
and spacing
) and
formatting it to kebab-case.
Next, an object is created/updated with the configuration key as the key and the class-setting (classFromTokenKey-tokenValue) pair as the value.
result = {
...result,
[configKey]: {
...result[configKey],
[className]: setting,
},
};
This is to match the default Tailwind theme’s shape.
Finally, the result
object is merged into the default Tailwind
theme (which is
synced via vendor-copy).
const newConfig = { theme: result };
if (extend) {
return merge(defaultConfig, newConfig);
}
function customizer(defaultConfigValue, newConfigValue) {
if (typeof defaultConfigValue === "object") {
return newConfigValue;
}
}
return mergeWith(defaultConfig, newConfig, customizer);
Optionally, the default Tailwind config’s theme can be merged deeply merge. By default, it does a shallow merge.
You can explore the tests to get a sense of this.
A gotcha was that Tailwind’s font-size
configuration is an array
containing the font-size and its matching line-height.
This had to be handled uniquely along with the font-family configuration which is an array of strings.
An example call use of this function would look like this:
import getTailwindConfig from "@tempera/tailwind-config;
import * as tokens from "./tokens";
const { theme } = getTailwindConfig(tokens);
// do something with the theme
View the source on GitHub:
https://github.com/michaelmang/tempera/tree/master/packages/tailwind
Tooling fo Component Tokens
With an existing Tailwind configuration, my next thought was to create tooling that allows you to get the Tailwind classes from a group of component tokens.
For some time, I have been using twind in place of the official Tailwind tool as it gets the Tailwind classes “just in time” instead of exporting a large CSS file upfront.
The Tailwind classes are fetched via a tw
instance:
import { tw } from 'https://cdn.skypack.dev/twind'
document.body.innerHTML = `
<main class="${tw`h-screen bg-purple-400 flex items-center justify-center`}">
<h1 class="${tw`font-bold text(center 5xl white sm:gray-800 md:pink-700)`}">This is Twind!</h1>
</main>
`
This means you can have a separate package for all your Tailwind configuration
that exports a tw
instance with a low cost (~12KB).
Ironically, I began writing this post just in time for
Tailwind’s announcement of their just-in-time (JIT) compiler
which is just (unofficially) implementing the twind
model into
their official tooling.
Anyways, the first step to my solution to getting Tailwind classes from
component tokens was to create an API that returns a
tw
instance that is
setup with a custom theme matching the design tokens using the previous
tool we just discussed.
View the source on GitHub:
https://github.com/michaelmang/tempera/tree/master/packages/twind/base
Then, I created another API that (in my mind) would return an object with properties for each component and the Tailwind classes needed to style it, as represented by the component tokens.
There were a couple of things to think about.
One was how to organize component classes by their base and variant classes.
Just a week ago,
Luke Jackson posted a tweet
notifying of a
twind/style
module which provides style
module.
The style
module is a function that receives an object mapping of
a component’s base and variants to their respective utility classes and
returns a function.
The return function will return the composition of Tailwind classes matching the variant that is requested by the incoming “props.”
import { tw, style } from 'twind/style'
const button = style({
// Define the base style using tailwindcss class names
base: `rounded-full px-2.5`,
// Declare all possible properties
variants: {
// button({ size: 'sm' })
size: {
sm: `text-sm h-6`,
md: `text-base h-9`,
},
// button({ variant: 'primary' })
variant: {
gray: `
bg-gray-400
hover:bg-gray-500
`,
primary: `
text-white bg-purple-400
hover:bg-purple-500
`,
},
},
})
// Customize the style
tw(button({ variant: 'primary', size: 'md' }))
// => rounded-full px-2.5 text-white bg-purple-400 hover:bg-purple-500 text-base h-9
After seeing this, my solution for my API clicked.
It would receive an official set of design tokens.
Then, it would generate a tw
instance with custom utility classes
that could apply the values of component tokens. This would allow for the
composition of custom utility classes.
Next, it would look at each component token and determine 1) the type (i.e. base or variant) and 2) the matching utility class.
The result is an object with a twind/style
instance for each
component (irrespective of variants).
This may then be used to “stitch” styles from a design system to a headless UI component library.
You can see a basic demo on CodeSandbox:
https://codesandbox.io/s/temperastitches-0ogls
At a low-level, the tool works by iterating through each component token and determining the component name, variant type, and UI state by parsing a design token key with the following format:
// base format
export const Component[COMPONENT]Base[LONGHAND_CSS_PROPERTY];
// size format
export const Component[COMPONENT][SIZE][LONGHAND_CSS_PROPERTY];
// variant format
export const Component[COMPONENT][VARIANT][UI_STATE][LONGHAND_CSS_PROPERTY];
Ideally, this format could be generated by a custom Style Dictionary format. However, I have not built that.
With knowledge of the component name, variant type, and UI state, it could
shape the object that the style
module from
twind/style
expects for every component (i.e. button or input).
The trickiest part was
getting the matching Tailwind class name. The reason this is a challenge is that Tailwind has a unique mapping from a
CSS property (i.e. font-size
) to the utility class (i.e.
text-2xl
).
To solve this problem, I created a script to map a Tailwind plugin (aka a CSS property) to the utility class prefix (the source code of Tailwind calls this a “name class”):
export const Animation = "animate";
export const BackgroundColor = "bg";
export const BackgroundImage = "bg";
export const BackgroundPosition = "bg";
export const BackgroundSize = "bg";
export const BorderColor = "border";
export const BorderRadius = "rounded";
export const BorderWidth = "border";
export const BoxShadow = "shadow";
export const Cursor = "cursor";
export const DivideColor = "divide";
export const Fill = "fill";
export const Flex = "flex";
export const FontSize = "text";
export const GradientColorStops = "from";
export const Inset = "inset";
export const Margin = "m";
export const Outline = "outline";
export const Padding = "p";
export const PlaceholderColor = "placeholder";
export const RingColor = "ring";
export const RingWidth = "ring";
export const Stroke = "stroke";
export const Color = "text";
export const TransitionProperty = "transition";
Then, the utility class is formed by taking the “name class” of the determined
plugin (i.e. fontSize
) and appending the key of the property in
the Tailwind configuration with a value matching the design token (i.e.
bg-primary-red
).
If a UI state is determined, it appends it to the final class name (i.e.
hover:bg-primary-red
).
🎉 Cool!
The power of this is that it would (in theory) allow you to automatically
generate the twind/style
instances for all your components
specified in the design tokens.
Then, you would then just have to wire up these style functions to a UI component in your headless UI component library.
In React, for example, the arguments of the style function would map to the component’s props:
import getStitches from '@tempera/stitches';
import getTwind from '@tempera/twind';
import { style } from 'twind/style'
import tokens from './tokens';
const stitches = getStitches(tokens);
const { tw } = getTwind(tokens);
const Button = ({ variant = "primary", size = "md" }) => {
return (
<button className={tw(styles.button({ variant, size }))}>
...
</button>
);
};
View the source code on GitHub:
https://github.com/michaelmang/tempera/tree/master/packages/twind/stitches
Conclusion
This is super experimental, but once again, I hope it brings inspiration for ways to improve tooling around design tokens.
Many thanks to the Twind team for the ideas and libraries used in this experiment.
As always, discuss, pow, and share.