DEV Community

Cover image for Strategy to generate the Tokens Studio(Figma) JSON file converter for Tailwind CSS and Emotion
Brandon Wie
Brandon Wie

Posted on

Strategy to generate the Tokens Studio(Figma) JSON file converter for Tailwind CSS and Emotion

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;
};
Enter fullscreen mode Exit fullscreen mode

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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');
}
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

The theme for Emotion provider is almost ready

  1. now you only need to convert the boxShadow values to use it inside css(@emotinoa/react) by concatenating the values following the CSS format
  2. 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}>
Enter fullscreen mode Exit fullscreen mode

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]};
  `
);
Enter fullscreen mode Exit fullscreen mode

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',
           },
         ],
       },
     },
   };
Enter fullscreen mode Exit fullscreen mode
  • 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',
       }),
     ],
   }
Enter fullscreen mode Exit fullscreen mode
  • 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)