My new ebook  Design Systems for Developers  is here! Start reading

Design

Stitching Styles to a Headless UI Using Design Tokens and Twind

Last Updated: 2021-03-16

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.

stitching styles to a headless ui using design tokens and twind

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

stitching styles to a headless ui using design tokens and twind

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.

Design Systems for Developers

Read my latest ebook on how to use design tokens to code production-ready design system assets.

Design Systems for Developers - Use Design Tokens To Launch Design Systems Into Production | Product Hunt

Michael Mangialardi is a software developer specializing in UI development with React and fluent in UI/UX design. As a survivor of impostor syndrome, he loves to make learning technical skills digestible and practical. Formerly, he published articles, ebooks, and coding challenges under his brand "Coding Artist." Today, he looks forward to using his mature experience to give back to the web development community. He lives in beautiful, historic Virginia with his wife.