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

Design Tokens integration with CSS Modules: 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

CSS Modules do not require installation in most modern cases. Project templates or frameworks like NextJS can take care of processing for seamless and effortless experience. You can further enhance CSS modules by using SASS or similar pre-processor.

Our project is based on Vite template and CSS modules are available without additional setup.

Along with CSS Modules we’ll 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! 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

CSS Modules are not much different from “vanilla” CSS, however it’s much more convenient in use and reliable in regards of stylesheets output.

Have a look at very basic component:

// excerpt from Header.tsx
import styles from './header.module.css';

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

  return (
    <header className={styles.root}>
      <h1>Dystopian Weather</h1>
      <div>{children}</div>
    </header>
  );
};

Styles are located in a neighbor file with *.module.css postfix. If you are using SCSS it would respectively change to *.module.scss.

Like the component, styles are minimal:

.root {
  position: relative;
  z-index: 20;

  display: flex;
  flex-flow: row nowrap;
  align-items: center;

  padding: var(--awsm-space-200) var(--awsm-space-100);
  border-bottom: var(--awsm-space-050) solid rgba(var(--awsm-color-primary-rgb), .5);

  background-color: var(--awsm-color-base-dark);
  color: var(--awsm-color-contrast-dark);
}

.root h1 {
  flex: 1 1 auto;

  margin: 0;
}

Note that since we’re using CSS Modules, classnames naming is arbitrary. All that matters from technology perspective is the correct reference from the styles object. However, from the maintenance standpoint classnames should make sense.
Traditionally the topmost element has the .root classname, yet in this case it can be a .header as well. Instead of .root h1 we could use .heading and assign it to h1 like this:

<header className={styles.root}>
  <h1 className={styles.heading}>Dystopian Weather</h1>
  <div>{children}</div>
</header>

Styling Headless UI components is not very different, since they allow classnames. 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={`${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, however for more flexible classnames handling 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

Since CSS Modules are very CSS-friendly, using CSS variables (CSS custom properties) is a native approach to Design Tokens integration.

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;
}

With that in place simply reference the variables in your stylesheets:

/* 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;
}

Conclusion

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

Read more about Headless UI integration in the respective guide.