Design Tokens Generator: design-tokens.dev Design Tokens with Tailwind CSS

Design Tokens integration with Tailwind CSS: Tutorial and Example Application for Frontend Developers

It is recommended to check the intro article before diving in specifics of the current guide. Source code for this and other example applications can be found on Github.

Overview and Prerequisites

Tailwind CSS is a utility-first (aka “functional”) CSS framework packed with classes like flex, pt-4, text-center and rotate-90 that can be composed to build any design, directly in your markup.

Tailwind installation is pretty straightforward:

npm install tailwindcss

The most important step is initialization and configuration. We’ll have a look at it further. Check out the official guide as well.

For this demonstration we’ll also be using Headless UI, that would help to build a couple of components. Headless UI is completely unstyled, fully accessible UI components collection, designed to integrate beautifully with Tailwind CSS. It is available for React and Vue.

Install Headless UI:

npm install @headlessui/react

And that’s it for installation step! Let’s create our components!
Explore the source code in DTG Examples repository.
Current guide refers to the following dependencies versions, latest at the moment of writing:

"@dtg-examples/common-tokens": "1.0.0",

"@headlessui/react": "1.7.15",
"vite": "4.3.9"

Typical Component

It’s very easy to get a hold of Tailwind (pun not intended) starting with the concepts study. Have a look at very basic component:

// excerpt from Header.tsx

const Header = (props: HeaderProps) => {
  const { children } = props;

  return (
    <header className="relative flex flex-row items-center z-20 py-8 px-4 bg-base-dark text-contrast-dark border-b-8 border-b-primary-alpha/50">
      <h1 className="flex-auto m-0 text-2xl leading-tight">Dystopian Weather</h1>
      <div>{children}</div>
    </header>
  );
};

First impression is “…it’s a whole lot of classnames… messy”.
Sure, it takes some time to accommodate. However we don’t need to use traditional CSS with some foundational exceptions.

And if you enhance your IDE with proper plugins/extensions, you won’t need to remember all those classnames.

But still none of those are the reasons that would urge one to use Tailwind CSS.
It’s the abstraction layer that it provides. We’ll have a look at it in a moment.


Let’s check how Tailwind is used with Headless UI (it’s a recommended way).
Have a look at List component:

// excerpt from List.tsx

const List = (props: ListProps) => {
  const { clsx, name, value, items, onSelectValue } = props;

  return (
    <RadioGroup
      value={value}
      onChange={onSelectValue}
      name={name}
      className={`p-4 flex flex-col justify-items-stretch justify-stretch bg-gamma-300 ${
        clsx ? ' ' + clsx : ''
      }`}
    >
      {items.map(({ uid, city, code, temp }) => (
        <RadioGroup.Option key={uid} value={uid} as={Fragment}>
          {({ checked }: { checked: boolean }) => (
            <div
              className={`group py-3 px-4 flex-auto focus-visible:outline-primary ${
                checked
                  ? 'bg-secondary-contrast text-secondary cursor-default z-10'
                  : 'bg-secondary text-secondary-contrast cursor-pointer hover:bg-secondary-tint'
              }`}
            >
              <RadioGroup.Label
                className={`block text-lg font-bold ${
                  !checked && 'group-hover:cursor-pointer'
                }`}
              >
                {city}
              </RadioGroup.Label>
              <RadioGroup.Description className="m-0 text-base">
                {weather[code]}: {temp}°C
              </RadioGroup.Description>
            </div>
          )}
        </RadioGroup.Option>
      ))}
    </RadioGroup>
  );
};

The code is straightforward, there are couple of things worth noting:

  • state of items (i.e. checked) are controlled via function passed as children
  • I highly recommend to use clsx (don’t confuse with the prop :)) for more flexible classnames handling in larger applications; ternary expression and string templates only work for simplest cases, but with Tailwind classnames abundance it goes out of control very quickly

Design Tokens Integration

Let’s have another look at our Header component:

// excerpt from Header.tsx

<header className="relative flex flex-row items-center z-20 py-8 px-4 bg-base-dark text-contrast-dark border-b-8 border-b-primary-alpha/50">
  <h1 className="flex-auto m-0 text-2xl leading-tight">Dystopian Weather</h1>
  <div>{children}</div>
</header>

Some of those classnames are evidently generic (functional, i.e. relative), but most of the time semantic classnames (py-8, bg-base-dark etc.) would dominate the scene.

In this example relative would transform into position: relative and py-8 is nothing but padding in vertical direction (hence, y-axis) with some value. Those values are configurable and customizable, and in this case it refers to the following code snippet:

.py-8 {
  padding-top: var(--awsm-space-200);
  padding-bottom: var(--awsm-space-200);
}

Not only that, but bg-base-dark and text-contrast-dark are semantic classnames, relying on our own custom values. Note that they are not a part of original Tailwind configuration, yet work seamlessly perfect. Configured in a particular fashion, Tailwind allows to have an abstraction layer for design tokens integration.

So how to make it work?
After installation you’ll need to initialize and configure Tailwind in just a couple of steps:

  • npx tailwindcss init
  • update tailwind.config.js file

Here’s an excerpt of the said file to give you an idea:

export default {
  theme: {
    // overriding original naming
    colors: {
      primary: {
        DEFAULT: 'var(--awsm-color-primary)',
        contrast: 'var(--awsm-color-primary-contrast)',
        shade: 'var(--awsm-color-primary-shade)',
        tone: 'var(--awsm-color-primary-tone)',
        tint: 'var(--awsm-color-primary-tint)',
        alpha: 'rgba(var(--awsm-color-primary-rgb) , <alpha-value>)',
      },
    },

    // preserving original naming
    lineHeight: {
      reset: '0',
      none: '1',
      tight: 'var(--awsm-line-height-tight)',
      snug: 'var(--awsm-line-height-tight)',
      normal: 'var(--awsm-line-height-regular)',
      relaxed: 'var(--awsm-line-height-loose)',
      loose: 'var(--awsm-line-height-loose)',
    },

    // preserving original naming
    borderRadius: {
      DEFAULT: 'var(--awsm-radius-medium)',
      none: '0',
      sm: 'var(--awsm-radius-small)',
      md: 'var(--awsm-radius-medium)',
      lg: 'var(--awsm-radius-large)',
      xl: 'var(--awsm-radius-round)',
      full: 'var(--awsm-radius-pill)',
    },
  }
}

Explore the full configuration file for details.
Also feel free to use it as a starting point for your own integration.

It’s worth mentioning that current approach to configuration is arbitrary and very malleable. You can overwrite a lot of properties for your benefit or you can keep them for the sake of maintainability, i.e. when other developers coming from “vanilla” Tailwind would work with your codebase. Here I took a somewhat balanced approach, completely replacing palette tokens, but keeping and merely updating values for others.

To setup global design tokens and core styles you need to work with some CSS after all.
For example, in our application the index.css is imported at the application top level. The structure for index.css and other styles can be found in the common project.

Tokens can be (re-) generated with ease and suit majority of our needs:

/* excerpt from tokens.css */

/* Pal🎨tte */

:root {
  /* primary */
  --awsm-color-primary: #9d0fbd;
  --awsm-color-primary-rgb: 157, 15, 189;
  --awsm-color-primary-contrast: var(--awsm-color-contrast-dark);
  --awsm-color-primary-contrast-rgb: 240, 240, 240;
  --awsm-color-primary-tint: #c270d4;
  --awsm-color-primary-shade: #67187a;
  --awsm-color-primary-tone: #9748a9;

  /* secondary */
  --awsm-color-secondary: #efcb26;
  --awsm-color-secondary-rgb: 239, 203, 38;
  --awsm-color-secondary-contrast: var(--awsm-color-contrast-light);
  --awsm-color-secondary-contrast-rgb: 18, 18, 18;
  --awsm-color-secondary-tint: #f9dc7a;
  --awsm-color-secondary-shade: #998222;
  --awsm-color-secondary-tone: #ccb151;
}

Conclusion

Using Tailwind CSS and Headless UI with tokens created by Design Tokens Generator is simple, effective and requires only basic installation with initial setup. This step is crucial for all future work, especially when you work in a team, so it makes sense to take some time here. Start with the example configuration file for reference and experiment further.

Read more about Headless UI integration in the respective guide.