Design Tokens Generator: design-tokens.dev Design Tokens with Headless UI

Design Tokens integration with Headless UI: 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

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! 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

We’ll be using Headless UI for creating two components for our app.
There are couple of reasons for that:

  • Headless UI exposes only certain components, not focusing on generics like Flex or Container
  • for the needs of our app we don’t need extra elements

The components that we’ll be adding are List (cities list) and Select (theme selector). Since they are very similar structure- and development-wise let’s overview only List.

// excerpt from List.tsx

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

  return (
    <RadioGroup
      value={value}
      onChange={onSelectValue}
      name={name}
      className={`${styles.root}${clsx ? ' ' + clsx : ''}`}
    >
      {items.map(({ uid, city, code, temp }) => (
        <RadioGroup.Option key={uid} value={uid} as={Fragment}>
          {({ checked }: { checked: boolean }) => (
            <div
              className={
                checked ? `${styles.item} ${styles.__checked}` : styles.item
              }
            >
              <RadioGroup.Label className={styles.label}>
                {city}
              </RadioGroup.Label>
              <RadioGroup.Description className={styles.description}>
                {weather[code]}: {temp}°C
              </RadioGroup.Description>
            </div>
          )}
        </RadioGroup.Option>
      ))}
    </RadioGroup>
  );
};

The code is very straightforward, there are just a couple of points that worth mentioning:

  • styling in current implementation is controlled by CSS modules, however since it’s headless UI, it’s very flexible in terms of CSS integration, so it can be easily swapped with your preferred way as well
  • state of items (i.e. checked) are controlled via function passed as children
  • to combine classnames I recommend to use clsx (don’t confuse with the prop :)) in larger applications; ternary expression and string templates only work for simplest cases, but even those are hard to maintain

Design Tokens Integration

For this demo we are using the most CSS-friendly approach of Design Tokens integration - CSS Modules.
It is very simple to start with and it does not require any specific knowledge apart from CSS itself. Also a lot of frontend templates support technology out of the box. Here we don’t need to setup additionally any processing, as Vite got us covered.

In order to use CSS Modules create a file named *.module.css (or *.module.scss if you need additional SCSS processing) and import it as style in the component file:

// excerpt from List.tsx

import styles from './list.module.css';

Then simply use classnames on the respective component parts,
for example for the List Item:

// excerpt from List.tsx

<div className={checked ? `${styles.item} ${styles.__checked}` : styles.item}>
  ...
</div>

In the stylesheet we have the following styles that match this element:

/* excerpt from list.module.css */

.item {
  flex: 1 1 auto;

  padding: var(--awsm-space-075) var(--awsm-space-100);
  background: var(--awsm-color-secondary);

  color: var(--awsm-color-secondary-contrast);
  transition: all ease-out var(--awsm-duration-short);
  cursor: pointer;
}

.__checked {
  z-index: 1;
  color: var(--awsm-color-secondary);
  background: var(--awsm-color-secondary-contrast);
  cursor: default;
}

As you can see we simply use Design Tokens in the form of CSS variables.
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.

Conclusion

Using Headless UI and CSS Modules with tokens created by Design Tokens Generator is simple and straightforward.
Headless UI provides a number of out-of-the-box components that can be styled completely according to your design needs. Combined with CSS Modules you get very lean, extensible and beginner-friendly setup.

Headless UI allows for flexibility that’s often missing in other frameworks. You can easily integrate a component or several components into your application or UI library with only the cost of styling integration. However if you are already using Tailwind CSS, you are fully covered. Check out the respective guide for details.