Preprocessing Design System Tokens in CSS-in-JS
Don’t throw away your design system tokens!
You’ve decided to change your design system styles from LESS or SASS to a CSS-in-JS approach for its many advantages, but you’ve run into a problem.
The Problem
Your existing design system is built upon a library of design tokens that are being preprocessed in LESS or SASS to make your CSS styles consistent. The whole look of your design system is based on these tokens and with CSS-in-JS, you’ve lost the ability to use your tokens and preprocess them.
Or have you?!
One Way to Live
Let’s create a small example and say our tokens look like this:
One thing we CAN do to solve our problem is this: Create and distribute a tokens package to be consumed as a dependency for our components.
Let’s say this is a styles.js file for our banner
component that imports that tokens
package:
While that will work, it’s less than ideal because teams that are consuming our packages will not necessarily update all packages at once. That means we will almost certainly end up with many very slightly different versions of our tokens package in the app bundle where there is lots of overlap.
For example, let’s say a team is using v1.0.0 of our button
and banner
packages which both use v1.0.0 of our tokens
package as a dependency.
Now that team decides to update its banner
package to v1.1.0 which has an updated dependency of tokens
v1.1.0.
It would not be uncommon to see the tokens
package in that team’s bundle twice. Imagine if that team were consuming a very realistic number of say 20 packages instead of just two!
In the end, we’ve traded one problem for another. That’s one way to live, but there’s a better way.
The Real Solution
We can preprocess our CSS-in-JS files with css-in-js-preprocessor
:
It works by using the same concept that you’re familiar with for LESS and SASS. To illustrate that, let’s modify our previous example:
First of all, you’ll need to install css-in-js-preprocessor
:
npm i css-in-js-preprocessor --save-dev
This time around, our tokens are going to live in a file in our project called my-tokens.json. The content is the same as above, it’s just in a file instead of a package. Now, we change the import of our banner
style.js file to import a local file:
Next is where the magic happens. We’re going to create a style-preprocess.js file which will be executed at the end of our compile or build process on the files that are outputted from that process:
How you execute this file in your pipeline is up to you, but we typically do it as part of a compile script that we execute as an NPM command in our package.json:
"scripts": {
"compile": "tsc && node ./style-preprocess.js"
}
The tsc
command is to compile TypeScript. If you’re not using TypeScript, you are most likely using Babel. Just add on our new script to the end of whatever you’re doing to transpile your JavaScript.
WARNING: Make sure you run the script against the files the build process outputs and not against your source files!
What we should see is that our styles.js file changed from this:
…to this:
BAM!
We have successfully preprocessed our files by replacing the references to the tokens with the actual token values! Not only that, but css-in-js-preprocessor
has removed the token import from the top of our file!
Our original source file for styles.js should be unchanged and using the tokens. The benefit is just like it was with LESS and SASS: if the token value ever changes, we simply recompile and our output now has the updated values.
No dependencies or versions to manage. No duplicated code in our bundle. No muss. No fuss.
Multiple Files
In our real life code, we’re almost certainly going to need to preprocess more than one file. Well, the preprocessor
conveniently returns a function so we can setup our base preprocessor one time and loop through the files we want to preprocess:
Custom Preprocessors
You’ll find that css-in-js-preprocessor
is even more more powerful and can be used in many more ways and for many more things than our old LESS and SASS counterparts. This is because we can inject our own custom preprocessors.
Let’s create two custom preprocessors and add them to the preprocessor
util. This replaces lines #8–9 above:
If we re-run our compile, we should see that our styles.js file has changed to this:
We can see that padding
has been transformed to margin
and a date/time stamp has been added as a comment to the bottom of the file due to our custom additions.
This is obviously a contrived example, but it illustrates how you can do anything you want. You are limited by only your imagination.
For More Than Just CSS-in-JS Files
Because we can add our own custom preprocessors, we can actually use this for more than just our design tokens and CSS-in-JS.
For example, one thing I often do is create a version
constant in my component files that gets added to the outermost DOM node of a component as a data attribute when it’s in debug mode. This version constant gets updated during my bump process and runs against .jsx
and .tsx
files.
Let’s see LESS and SASS do that!
Onward
We can now move our design system into the new age of CSS-in-JS, reap all of its benefits, not have any drawbacks, and gain additional horsepower & flexibility along the way!