デザイントークンを Storybook に落とし込んでみる

![スクリーンショット 2023-01-31 20.22.50.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/50a766cb-a429-8e31-925d-7cbb06f252d4.png)

「デザイン -> 開発」のつなぎとして、デザインファイルのデザイントークンを JSON ファイルに変換、さらに CSS / TypeScript へ変換、Storybook でプレビューまでやってみたので解説。

特に JSON から CSS / TypeScript に変換する際の、Style Dictionary でのカスタマイズ設定は厚めに記載しています。

### 作ったもの

▷ Storybook：デザイントークンのプレビューを作成しました。

![スクリーンショット 2023-01-31 21.51.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/81a0a071-7a88-996f-ea66-2ef90ad22327.png)

▷ コード：Style Dictionary でのカスタマイズ設定などご参考までに。

https://github.com/tenugui-taro/qiita_figma_to_storybook__public

### 使用技術

- [Figma](https://www.figma.com/ja/design/)：効率よく UI 設計やプロトタイプ作成が可能。個人的にコンポーネント駆動開発と相性◎
- [Design Tokens](https://www.figma.com/community/plugin/888356646278934516/Design-Tokens)：Figma で登録したデザイントークンを JSON ファイルとして出力できる Figma プラグイン
- [Style Dictionary](https://amzn.github.io/style-dictionary/#/)：JSON 形式のデザイントークンを css / ios / javascript など様々なプラットフォームで扱えるよう変換。
- [Storybook](https://storybook.js.org)：コンポーネント駆動開発ツール。アプリとは別の独立環境でコンポーネントごとに挙動確認できる。

### Figma のデザイントークンを JSON ファイルへ変換する 

Figma プラグインの「[Design Tokens](https://www.figma.com/community/plugin/888356646278934516/Design-Tokens)」を用いて、登録したカラースタイルを JSON ファイルとして出力します。 

![スクリーンショット 2023-01-31 4.44.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/cc25887f-f510-c346-2e59-f57f5c8708c2.png)

今回はカラースタイルが対象なので、「Colors」を選択して「Save & Export」から JSON ファイルを出力します。

![スクリーンショット 2023-01-31 4.54.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/11b32c21-a5a3-a863-897c-dffa324fe2b7.png)

実際に出力された JSON ファイルはこちら 

[figma-color.tokens.json](https://github.com/tenugui-taro/qiita_figma_to_storybook__public/blob/main/src/plugins/style-dictionary/tokens/figma-color.tokens.json)

### Style Dictionary で JSON ファイルを CSS / TypeScript へ変換する

「[Style Dictionary](https://amzn.github.io/style-dictionary/#/)」を用いて、デザイントークンを CSS / TypeScript で扱えるように変換します。

![スクリーンショット 2023-01-31 5.13.13.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/c9ee45fc-633f-d6b8-c9b6-d9ca6f78a91a.png)

Style Dictionary の設定に関する記載が長くなってしまったので、下記からご興味あるものを参照ください。

<details><summary>インストール</summary>

[Style Dictionary - Installation](https://amzn.github.io/style-dictionary/#/quick_start?id=installation)

Create React App を用いてプロジェクト作成し、dev dependency としてインストールします。

```
$ npx create-react-app my-app --template typescript
$ npm install -D style-dictionary
```

また、公式で提供されている [Basic サンプル](https://github.com/amzn/style-dictionary/tree/main/examples/basic) を
下記コマンドでプロジェクトに取り込むことができます。

```diff:package.json
  "scripts": {
+   "style-dictionary:init": "style-dictionary init basic"
  }
```

```
$ npm run style-dictionary:init
```

</details>

<details><summary>CSS / TypeScript 変換設定</summary>

Basic サンプルでは、scss / ios / android など様々なプラットフォームに変換されていました。 
こちらの設定は、[config.json](https://github.com/amzn/style-dictionary/blob/main/examples/basic/config.json)にて行われています。

今回は、CSS / TypeScript に変換したいのでドキュメントを参照しながら`config.json`を修正します。

- [Style Dictionary - Platform](https://amzn.github.io/style-dictionary/#/config?id=platform) 
- [Style Dictionary - Transform Groups](https://amzn.github.io/style-dictionary/#/transform_groups)
- [Style Dictionary - Pre-defined Formats](https://amzn.github.io/style-dictionary/#/formats?id=pre-defined-formats)

```json:config.json
{
  "source": ["tokens/**/*.json"],
  "platforms": {
    "css": {
      "transformGroup": "css",
      "buildPath": "build/css/",
      "files": [
        {
          "destination": "_variables.css",
          "format": "css/variables"
        }
      ]
    },
    "typescript": {
      "transformGroup": "js",
      "buildPath": "build/ts/",
      "files": [
        {
          "format": "javascript/module",
          "destination": "tokens.js"
        },
        {
          "format": "typescript/module-declarations",
          "destination": "tokens.d.ts"
        }
      ]
    }
  }
}
```

```diff:package.json
  "scripts": {
+   "style-dictionary:build": "style-dictionary build"
  }
```

```
$ npm run style-dictionary:build
```

ビルドコマンドを実行して、CSS / TypeScript ファイルが出力されることを確認します。

![スクリーンショット 2023-01-31 12.14.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/cb8c4dfd-b841-0362-9b73-01c27359d770.png)

</details>

<details><summary>詳細な変換設定（命名規則や単位など / 独自の型定義）</summary>

#### 命名規則や単位などの設定

[Style Dicitonary - Transform groups - js](https://amzn.github.io/style-dictionary/#/transform_groups?id=js) を参照すると、詳細な変換設定は「attribute/cti name/cti/pascal size/rem color/hex」となっています。 

今回は名前をケバブケースに変換してほしいので「name/cti/pascal -> name/cti/kebab」に修正します。
[Style Dicitonary - Transforms](https://amzn.github.io/style-dictionary/#/transforms?id=transforms) を参照し、`config.json`を下記のように修正しました。

```diff:config.json
    "typescript": {
-      "transformGroup": "js",
+      "transforms": [
+        "attribute/cti",
+        "name/cti/kebab",
+        "size/rem",
+        "color/hex"
+      ],
      "buildPath": "build/ts/",
      "files": [
        {
          "format": "javascript/module",
          "destination": "tokens.js"
        },
        {
          "format": "typescript/module-declarations",
          "destination": "tokens.d.ts"
        }
      ]
    }
```

ビルドコマンドを実行すると、出力される名前が修正されたことが確認できます。

```diff:tokens.js
module.exports = {
  "color": {
    "base": {
      "gray": {
        "light": {
          "value": "#cccccc",
-          "name": "ColorBaseGrayLight",
+          "name": "color-base-gray-light",
```

また他も同様に、サイズを`px`単位で取得した場合は「size/rem -> size/px」と修正することで設定できます。

#### 独自の型定義の設定 

[Style Dicitonary - Pre-defined Transforms - typescript/module-declarations](https://amzn.github.io/style-dictionary/#/formats?id=typescriptmodule-declarations) 

既存の変換設定では 100% 正確な型が生成されることが出来ません。

> As you can see above example output this does not generate 100% accurate d.ts. This is a compromise between of what style-dictionary can do to help and not bloating the library with rarely used dependencies.

例として、カラートークンの用途を説明する`description`プロパティが存在しても、生成される型には反映されません。 

```json:tokens/color/base.json
{
  "color": {
    "base": {
      "red": { "value": "#FF0000", "description": "ブランドカラーです。" },
    }
  }
}
```

```typescript:build/ts/tokens.d.ts
// `description`プロパティ反映されていない 
declare interface DesignToken {
  value: any;
  name?: string;
  comment?: string;
  themeable?: boolean;
  attributes?: {
    category?: string;
    type?: string;
    item?: string;
    subitem?: string;
    state?: string;
    [key: string]: any;
  };
  [key: string]: any;
}
```

回避策として、自前の型定義ファイルを参照するフォーマットを作成します。 

はじめに、`description`プロパティを含むよう修正した型定義を作成します。 

```typescript:CustomDesignToken.d.ts
interface DesignToken {
  value: any;
  name?: string;
  comment?: string;
  description?: string; // プロパティの追加
  themeable?: boolean;
  attributes?: {
    category?: string;
    type?: string;
    item?: string;
    subitem?: string;
    state?: string;
    [key: string]: any;
  };
  [key: string]: any;
}

// 型定義のエクスポート
export { DesignToken };
export interface DesignTokens {
  [key: string]: DesignTokens | DesignToken;
}

```

次に、既存の [typescript/module-declarations](https://github.com/amzn/style-dictionary/blob/main/lib/common/formats.js#L470) の実装をベースに、型定義ファイルを指定する箇所で自前の型定義ファイルを参照するフォーマットを作成します。
また、カスタマイズするために`config.json`から`config.js`へ置換しています。

```diff:config.js
StyleDictionaryPackage.registerFormat({
-  name: "typescript/module-declarations",
+  name: "typescript/custom-module-declarations",
 
   ...

    const designTokenInterface = fs.readFileSync(
-      path.resolve(__dirname, `../../types/DesignToken.d.ts`), {encoding:'UTF-8'}
+      path.resolve(__dirname, `./CustomDesignToken.d.ts`),
      { encoding: "UTF-8" }
    );
 
   ...

});

module.exports = {
  source: ["tokens/**/**.json"],
  platforms: {
    javascript: {
      files: [
        {
          format: "javascript/module",
          destination: "tokens.js",
        },
        {
-          format: "typescript/module-declarations",
+          format: "typescript/custom-module-declarations",
          destination: "tokens.d.ts",
        },
      ],
    },
  },
};

```
 
ビルドコマンドを実行すると、独自の型定義が反映されていることが確認できます。 

```diff:build/ts/tokens.d.ts
declare interface DesignToken {
  value: any;
  name?: string;
  comment?: string;
+  description?: string;
  themeable?: boolean;
  attributes?: {
    category?: string;
    type?: string;
    item?: string;
    subitem?: string;
    state?: string;
    [key: string]: any;
  };
  [key: string]: any;
}

+ export { DesignToken };
+ export interface DesignTokens {
+  [key: string]: DesignTokens | DesignToken;
+  }
```

</details>

### CSS / TypeScript から Storybook に落とし込む

![スクリーンショット 2023-01-31 20.31.02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/2b17b533-bcb2-3ce2-2897-c0785c85bfe2.png)

Story の実装にあたっては、デザイナー視点では「デザイントークンが再現されているか」、エンジニア視点では「**用意されているデザイントークンは何があるか**」「**デザイントークンの名前は？**」を意識し確認できるようにしています。

特に、開発を進めるうえで「**デザイントークンの名前**」は頻繁に使用するため、コピー可能なテキストとして実装しました。 
 
![画面収録_2023-01-31_20_10_28_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/1116978/bcc043e7-56e3-4e9f-1b7a-7f1d9400f2f9.gif)

### まとめ 

- デザイントークンの JSON 形式と、Style Dictionary が期待する JSON 形式が異なる場合、Style Dictionary カスタマイズ設定でフォローしてあげる
- メンテナンス性を向上させるために、Figma の更新と Storybook への反映を同期させる仕組みが必要
- ブランディングを意識したデザインシステム構築 / 運用に適していそう

### 参考　

- [WEB+DB PRESS Vol.129 - 小さく始めるデザインシステム](https://gihyo.jp/magazine/wdpress/archive/2022/vol129)
- [Narmi Design System](https://storybook.js.org/showcase/narmi-narmi-design-system)