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
boxShadow
values 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)