Strategy to generate the Tokens Studio(Figma) JSON file converter for Tailwind CSS and Emotion
Tags: tokensstudio, figma, tailwindcss, emotion
Published: 2023-10-05 07:20:53

# 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:

```tsx
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
// 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)

```jsx
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.

```jsx
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

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](https://developer.mozilla.org/en-US/docs/Web/CSS/box-shadow)
2. write JSON file with the object created, and you can use it directly as a theme type

```tsx
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

```tsx
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](https://tailwindcss.com/docs/font-size) configuration

   ```tsx
   // 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](https://tailwindcss.com/docs/typography-plugin) plugin

   ```tsx
   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!