【小ネタ】FigmaのVariablesを楽してそのままCSS変数にしたい

## きっかけ

Figmaの`variables`を最近やっと使い始めたのですが、

CSS変数のように色や数値に名前をつけたり、
`"/"` で区切るとグループで管理できたりとても便利ですよね。

https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma


これそのままCSS変数としてセットできればデザイナーとの認識違いも減って
実装がスムーズになるのでは？と思いやってみました。


## 結果
:::note info
FigmaプラグインでVariablesをJSONとして書き出し、
JSONをもとにCSS変数ファイルを作成できます。
:::

今回はスター数とか踏まえてこちらのプラグインでJSONを書き出しました。
[Export/Import Variables](https://www.figma.com/community/plugin/1256972111705530093/export-import-variables)


## 【余談】ボツ案
### Tokens Studio × Style Dictionary
`Variables` ではなくFigmaの [Tokens Studio](https://www.figma.com/community/plugin/843461159747178978/tokens-studio-for-figma-figma-tokens)プラグインを使って
デザイントークンの管理をするなら、ExportしたJSONを
[Style Dictionary](https://amzn.github.io/style-dictionary/#/README)でCSSに書き出してくれるので便利です。

Githubと連携して自動化などもできるみたいなので
ガチガチにデザインシステムを実装に組み込みたいならこの方法が良さそうです。

### 参考
https://zenn.dev/mi_/articles/453f7594120c9a


### じゃあ Variables × Style Dictionary ?
`Variables` をJSONで書き出しして、
Style DictionaryでCSS変換すればいいのでは と思ったのですが、

Tokens Studioとは違って
JSONをStyle Dictionaryで変換できる形にしてくれるツールがないので今回は諦めました...😢


## 実装
細かいことは無視してとりあえずVariablesが
CSS変数として書き出せるように実装しました。

### ①FigmaでVariablesを作成
Figmaでローカルバリアブルを作成します。

![画面収録 2024-06-16 15.56.41.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2612989/873b9401-79ed-af86-0974-7f3a6ffa3d07.gif)
- そのまま変数名になるのも意識して命名します
- JSONを書き出せるのがコレクションごとなので、**1コレクション内** で管理します

### ②書き出したJSONをトークン形式に変換
書き出したJSONそのままだと情報も多くわかりづらいです...
↓こんな感じ
```json:variables.json
  "variables": [
    {
      "id": "VariableID:1:6",
      "name": "semantic/color/black",
      "description": "",
      "type": "COLOR",
      "valuesByMode": {
        "1:1": { "type": "VARIABLE_ALIAS", "id": "VariableID:1:7" }
      },
      "resolvedValuesByMode": {
        "1:1": {
          "resolvedValue": {
            "r": 0.13333334028720856,
            "g": 0.13333334028720856,
            "b": 0.13333334028720856,
            "a": 1
          },
          "alias": "VariableID:1:7",
          "aliasName": "palette/black"
        }
      },
      "scopes": ["ALL_SCOPES"],
      "hiddenFromPublishing": false,
      "codeSyntax": {}
    }
  ]
```

#### トークンライクなJSONに変換
CSSにする前に、必要な情報だけ抜き取って
いったんわかりやすい形のJSONに変換します。

<details><summary><b>スクリプト（長くなるので折りたたみ）</b></summary>

```typescript: transformVariables.ts
import fs from 'fs';

interface Variable {
  id: string;
  name: string;
  type: string;
  resolvedValuesByMode: {
    '1:1': {
      resolvedValue: any;
      alias: string | null;
      aliasName: string | null;
    };
  };
}

interface FigmaJSON {
  id: string;
  name: string;
  modes: { [key: string]: string };
  variableIds: string[];
  variables: Variable[];
}

const inputFilePath = '.design-token/variables.json';
const outputFilePath = '.design-token/tokens.json';

// 小数点2以下は四捨五入
const roundToTwoDecimals = (value: number): number => {
  return Math.round(value * 100) / 100;
};

const setNestedProperty = (obj: any, path: string, value: any) => {
  const keys = path.split('.');
  keys.reduce((acc, key, index) => {
    if (index === keys.length - 1) {
      acc[key] = value;
      return;
    }
    if (!acc[key]) acc[key] = {};
    return acc[key];
  }, obj);
};

const convertFigmaJsonToStyleDictionary = (inputFile: string, outputFile: string) => {
  const rawData = fs.readFileSync(inputFile, 'utf8');
  const figmaJson: FigmaJSON = JSON.parse(rawData);

  const styleDictionaryTokens = figmaJson.variables.reduce((acc, variable) => {
    const { resolvedValue, aliasName } = variable.resolvedValuesByMode['1:1'];
    let value;

    if (variable.type === 'COLOR') {
      value = aliasName ? `{${aliasName.replace(/\//g, '.')}}` : `rgba(${roundToTwoDecimals(resolvedValue.r * 255)}, ${roundToTwoDecimals(resolvedValue.g * 255)}, ${roundToTwoDecimals(resolvedValue.b * 255)}, ${resolvedValue.a})`;
    } else {
      value = aliasName ? `{${aliasName.replace(/\//g, '.')}}` : roundToTwoDecimals(resolvedValue).toString();
    }

    const path = variable.name.replace(/\//g, '.');
    setNestedProperty(acc, path, { value });

    return acc;
  }, {} as { [key: string]: any });

  fs.writeFileSync(outputFile, JSON.stringify(styleDictionaryTokens, null, 2), 'utf8');
};

convertFigmaJsonToStyleDictionary(inputFilePath, outputFilePath);

```

</details>


#### 変換を実行

今回は `ts-node` を使って npm script から実行します。

```json:package.json
{
  "scripts": {
    "build-token": "ts-node scripts/transformVariables.ts"
  }
}
```

```bash
npm run build-token
```

スクリプトを実行したら、CSS変数を作成するための`tokens.json`が新たに作成されます。

#### 変換後のtokens.json
```json:tokens.json
{
  "semantic": {
    "color": {
      "black": {
        "value": "{palette.black}"
      },
      "primary": {
        "value": "{palette.orange}"
      },
      "accent-1": {
        "value": "{palette.blue}"
      },
      "accent-2": {
        "value": "{palette.blue-green}"
      }
    },
    "content-padding": {
      "value": "{space.lg}"
    }
  },
  "palette": {
    "black": {
      "value": "rgba(34, 34, 34, 1)"
    },
    "orange": {
      "value": "rgba(255, 103, 0, 1)"
    },
    "blue": {
      "value": "rgba(73, 109, 219, 1)"
    },
    "blue-green": {
      "value": "rgba(115, 210, 222, 1)"
    }
  },
  "typo": {
    "font-size": {
      "base": {
        "value": "16"
      },
      "lg": {
        "value": "18"
      },
      "sm": {
        "value": "14"
      }
    },
    "line-height": {
      "base": {
        "value": "1.5"
      },
      "lg": {
        "value": "1.8"
      }
    }
  },
  "space": {
    "default": {
      "value": "40"
    },
    "sm": {
      "value": "32"
    },
    "lg": {
      "value": "48"
    }
  }
}
```
JSONファイルを見てもわかるような形に変換できました。

### ③JSONからCSS変数ファイルを作成
あとはこの`tokens.json`ファイルをもとに、
CSS変数を定義したファイルが一発で作成されるようにします。

<details><summary><b>スクリプト（長くなるので折りたたみ）</b></summary>


```typescript:buildVariablesCSS.ts
import fs from 'fs';

const inputFilePath = '.design-token/tokens.json';
const outputFilePath = 'src/styles/variables.css';

const generateCssVariables = (obj: any, parentKey = ''): string => {
  return Object.entries(obj).map(([key, value]) => {
    const variableName = parentKey ? `${parentKey}-${key}` : key;
    if (typeof value === 'object' && value.value === undefined) {
      return generateCssVariables(value, variableName);
    }
    const cssValue = value.value.startsWith('{') ? `var(--${value.value.slice(1, -1).replace(/\./g, '-')})` : value.value;
    return `--${variableName}: ${cssValue};`;
  }).join('\n');
};

const convertTokensJsonToCssVariables = (inputFile: string, outputFile: string) => {
  const rawData = fs.readFileSync(inputFile, 'utf8');
  const tokensJson = JSON.parse(rawData);

  const cssVariables = generateCssVariables(tokensJson);

  const cssContent = `:root {\n${cssVariables}\n}`;

  fs.writeFileSync(outputFile, cssContent, 'utf8');
};

convertTokensJsonToCssVariables(inputFilePath, outputFilePath);

```

</details>

#### 全てのスクリプトを実行
先ほどの npm scriptに追加でこのスクリプトを実行します。
```json:package.json
{
  "scripts": {
    "build-token": "ts-node scripts/transformVariables.ts && ts-node scripts/buildVariablesCSS.ts"
  }
}
```

```bash
npm run build-token
```

スクリプトを実行すると、
無事にtokens.jsonをCSS変数に変換した
`src/styles/variables.css`ファイルが作成されました🙌🏻


#### 作成されたvariables.css
```css:variables.css
:root {
--semantic-color-black: var(--palette-black);
--semantic-color-primary: var(--palette-orange);
--semantic-color-accent-1: var(--palette-blue);
--semantic-color-accent-2: var(--palette-blue-green);
--semantic-content-padding: var(--space-lg);
--palette-black: rgba(34, 34, 34, 1);
--palette-orange: rgba(255, 103, 0, 1);
--palette-blue: rgba(73, 109, 219, 1);
--palette-blue-green: rgba(115, 210, 222, 1);
--typo-font-size-base: 16;
--typo-font-size-lg: 18;
--typo-font-size-sm: 14;
--typo-line-height-base: 1.5;
--typo-line-height-lg: 1.8;
--space-default: 40;
--space-sm: 32;
--space-lg: 48;
}

```

## 感想
よい感じのプラグインがなかったので試しに自力でやってみました。

`Variables`をただCSS変数にするだけなら
もうちょっと調整すればなんとかなるかも...

ただ
- 小数点の計算
- 単位の設定
- 並び順

など結構あやしいところもあって制御しづらいので、あまり実用的ではないかも。

<br>

`Variables`をいい感じにStyle Dictionaryで使える形に変換してくれる
プラグインが開発されたら嬉しいですね（他力本願）

### 参考
https://help.figma.com/hc/en-us/articles/15339657135383-Guide-to-variables-in-Figma

https://zenn.dev/beijaflor/articles/584eb4e00a5bb3