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)