Prologue
My team decided to use the Tokens Studio where each style is stored as a set of token(s). You can connect to a GitHub repository, which makes it easy to push as JSON files while enjoying the benefits of version control as well.
By creating a converter for Tailwind CSS and Emotion, we had a great experience using style variables with auto-completions.
I’ve tried to find commonalities in the token structure and create a general parser for the JSON files. However, I found it to be quite challenging because it’s heavily influenced by how the token system is structured and configured. Also, the designer's preference must be considered.
Therefore, I am going to explain a general approach to the task of generating a Tokens Studio JSON file converter for Tailwind CSS and Emotion.
Basic concept with types
Here are several base types from the JSON structure I worked with:
type Typography = {
fontFamily: string;
fontSize: string;
fontWeight: string;
lineHeight: string;
};
/** box-shadow value must be an array */
type BoxShadow = {
x: string; // number
y: string; // number
blur: string; // number
spread: string; // number
color: string; // reference to a color
type: string;
}[];
type Value = string | Typography | BoxShadow;
// tree
export type Node = {
[key: string]: Node | Token;
};
// last nodes of the tree
export type Token = {
value: Value;
type: string;
description?: string;
};
Step 1. Flatten the object
In this case, we had two JSON files generated by Tokens Studio: global and component. The global contains values that are used as references for other fields in the global itself and for the component-specific values.
// JSON example generated by Tokens Studio
{
"colors": {
"deepGreen": {
"25": {
"value": "#F6F9F8",
"type": "color"
}
// other deepGreen colors...
}
// other colors...
},
"typography": {
"heading": {
"h1": {
"value": {
"lineHeight": "130%",
"fontWeight": "{global.fontWeight.800}",
"fontFamily": "{global.fontFamily.KR}",
"fontSize": "{global.fontSize.40}"
},
"type": "typography"
}
// other headings...
}
// other typographies
},
"boxShadow": {
"xs": {
"value": {
"x": "0",
"y": "1",
"blur": "2",
"spread": "0",
"color": "{global.colors.black-opacity.5}",
"type": "dropShadow"
},
"type": "boxShadow"
},
"sm": {
"value": [
{
"x": "0",
"y": "1",
"blur": "3",
"spread": "0",
"color": "{global.colors.black-opacity.10}",
"type": "dropShadow"
},
{
"x": "0",
"y": "1",
"blur": "2",
"spread": "0",
"color": "{global.colors.black-opacity.6}",
"type": "dropShadow"
}
],
"type": "boxShadow"
}
// ...other boxShadows
}
}
A token contains value and type keys (has description as optional).
Firstly, for instance, we need to assign the value #F6F9F8 directly to the key 25.
There were two exceptions in my case while dealing with typography and boxShadow. Just remember to parse it accordingly depending on the libraries that you use respectively. (in the case of boxShadow, if you put more than one set of values, it will return an array, which will make your converter a bit more verbose)
export function flattenValueProperty(obj) {
for (let k in obj) {
if (isToken(obj[k])) {
if (typeof obj[k].value === 'string') {
// convert only
obj[k].value = convertUnit(obj[k].value);
}
// deal with typography exception
if (isObject(obj[k].value) && obj[k].value.hasOwnProperty('lineHeight')) {
obj[k].value.lineHeight = convertUnit(obj[k].value.lineHeight);
}
// assign value
obj[k] = obj[k].value;
} else {
flattenValueProperty(obj[k]);
}
}
return obj;
}
function convertUnit(value) {
if (value.includes('px')) {
return `${parseInt(value) / 16}rem`;
}
if (value.includes('%')) {
return `${parseInt(value) / 100}`;
}
return value;
}
export function isToken(arg) {
return isObject(arg) && arg.hasOwnProperty('value');
}
Step 2. Use preorder traversal for the referenced values, get and set the values
Let me introduce some of the functions that I used.
The referenced values are assigned as "color": "{colors.emerald.500}", and the path is colors.emerald[500]. Therefore, you just need to get the path from the string and retrieve the value from the path.
export function preorderTraversal(node, callback, path = []) {
if (node === null) return;
// callback with the node and the path to the node
callback(node, path);
// for boxShadow, if the node is array, run callback and end the recursive
if (isArray(node)) return;
// for each child of the current node
for (let key in node) {
if (typeof node[key] === 'object' && node[key] !== null) {
// Add the child to the path and recursively call the traversal
preorderTraversal(node[key], callback, [...path, key]);
}
}
}
export function getValueFromPath(obj, path) {
if (path.length === 0) return obj;
let val = obj;
for (let key of path) {
val = (val as any)[key];
}
return val;
}
export function setValueFromPath(obj, value, path) {
let currObj: any = obj;
for (let i = 0; i < path.length; i++) {
if (i === path.length - 1) {
currObj[path[i]] = value;
} else {
if (!currObj[path[i]]) {
currObj[path[i]] = {};
}
currObj = currObj[path[i]];
}
}
return obj;
}
The theme for Emotion provider is almost ready
- now you only need to convert the
boxShadowvalues to use it insidecss(@emotinoa/react) by concatenating the values following the CSS format - write JSON file with the object created, and you can use it directly as a theme type
import theme from './generated-emotion-theme.json';
export type Theme = typeof theme & { otherTheme: OtherTheme };
export const defaultTheme: Theme = { ...theme, otherTheme: otherTheme };
// You can use pass it to the theme prop of Emotion Provider
// <EmotionThemeProvider theme={defaultTheme}>
the variables that are converted can be used in Emotion styled component directly as
const StyledDiv = styled.div(
({ theme }) => css`
${theme.typography.body.body1.regular};
box-shadow: ${theme.boxShadow.sm};
color: ${theme.colors.deepGreen[25]};
`
);
There are a few more steps left for Tailwind CSS
Step 3. Typography and Box Shadow for Tailwind CSS
Typography
There are two ways to configure a typography in Tailwind CSS config
1. Use fontSize configuration
// https://tailwindcss.com/docs/font-size
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
fontSize: {
'2xl': [
'1.5rem',
{
lineHeight: '2rem',
letterSpacing: '-0.01em',
fontWeight: '500',
},
],
'3xl': [
'1.875rem',
{
lineHeight: '2.25rem',
letterSpacing: '-0.02em',
fontWeight: '700',
},
],
},
},
};
- recommend when using a single font throughout the application, or if there are limited cases for using other fonts, no plugin is required
- however, as you may notice, the parsing can be quite verbose due to the format
2. Use typography plugin
module.exports = {
theme: {
extend: {
typography: {
h1: {
css: {
lineHeight: '1.3',
fontWeight: '800',
fontFamily: 'Pretendard',
fontSize: '2.5rem',
},
},
// ...
},
},
}
plugins: [
require('@tailwindcss/typography')({
className: 'typography',
}),
],
}
- generally recommended
- you can use it as
<p className="typography-h1">Hello World!</p>
Box Shadow
- You can use the same concatenated string values that are generated when creating a theme JSON file for Emotion.
Hope you find it helpful.
Please comment below if you have any questions.
Happy Coding Y’all!
Top comments (0)