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.