🦄

Tokens Studio for Figma + Style Dictionaryを試してみた

2023/03/06に公開

はじめに 🏔

Tokens Studio for Figma(旧 Figma Tokens)で作成したデザイントークンをStyle Dictionaryで変換してみました。
Tokens StudioとStyle Dictionaryについての詳細は省きますが、ものすごく大まかに説明をすると、Tokens Studioは色やフォント等のデザイントークンを管理するためのFigmaプラグインで、Style DictionaryはiOSやWebなどの各プラットフォーム向けにデザイントークンを変換してくれるライブラリです。
詳細は以下ドキュメントを参考にしてください。
https://tokens.studio/
https://amzn.github.io/style-dictionary/#/

やってみよう 🫴🏻

おおまかな流れは以下です。

  • Tokens Studio for Figmaを用いてFigmaでデザインを作成
  • 作成したデザイントークン(json)を、GitHubのStyle Dictionary用のリポジトリにpush
  • pushされたjsonをtoken-transformerを用いてStyle Dictionary用のjsonに変換
  • 変換したjsonを、Style Dictionaryを用いて好みのフォーマットに出力
  • 出力されたデザイントークンをプロジェクトで使う

Tokens Studio for Figma でデザインを作成

Tokens Studioを使ってFigmaを作成します。
今回は、URLをリスト登録するWebアプリを想定して作成しました。
できたのがこちら。 (デザイナーではないのでFigmaに関しては大目に見てください🥺)

GitHubにデザイントークンのjsonをpush

Tokens Studioの左下にある矢印⬆️を押して、
push先のリポジトリ、コミットメッセージ、push先のブランチを指定してpushします。

Style Dictionary用のjsonに変換

Style Dictionary用のリポジトリでFigmaから生成されたjsonを変換していきます。
最終的なStyle Dictionary用のリポジトリの構成は以下です。

ディレクトリ構造
└── style-dictionary-sample/
    ├── build/
    │   ├── design-tokens.d.ts
    │   └── design-tokens.ts
    ├── dist
    ├── tokens/
    │   ├── figma-tokens.json
    │   └── style-dictionary-tokens.json
    ├── copy.ts
    ├── package.json
    └── style-dictionary.config.json

Style Dictionaryは、CTI(Category/Type/Item)という構造に基づいて設計されています。

参照:https://amzn.github.io/style-dictionary/#/tokens?id=category-type-item

そのため、まずはFigmaからエクスポートしたjsonをStyle Dictionary用にCTI構造のjsonに変換する必要があります。そこで、token-transformerというライブラリを使います。
https://www.npmjs.com/package/token-transformer?activeTab=readme

token-transformerをインストールします。

yarn add -D token-transformer

installしたら、package.jsonにscriptを追加します。

package.json
{
  ...
  "scripts": {
    "transform": "token-transformer tokens/figma-tokens.json tokens/style-dictionary-tokens.json",
  },
  ...
}

token-transformerコマンドの使い方は以下です。

token-transformer {変換元のjsonファイル} {変換後のjsonファイル}

これで、token-transformerを用いて変換ができます。

yarn transform

tokensフォルダの下にstyle-dictionary-tokens.jsonが作成されれば成功です。

Style Dictionaryを用いて任意のフォーマットに変換

続いて、Style Dictionaryを用いてjsonのデザイントークンをtsに変換します。
まずはStyle Dictionaryをインストールします。

yarn add -D style-dictionary

Style Dictionary用のconfigに以下のように記述します。
今回はplatformsにtsしかありませんが、iOSやAndroid用など複数の対応が必要な場合はplatformsに追加していきます。

style-dictionary.config.json
{
  "source": ["tokens/style-dictionary-tokens.json"],
  "platforms": {
    "ts": {
      "transformGroup": "js",
      "buildPath": "build/",
      "files": [
        {
          "format": "javascript/es6",
          "destination": "design-tokens.ts"
        },
        {
          "format": "typescript/es6-declarations",
          "destination": "design-tokens.d.ts"
        }
      ]
    }
  }
}

今回はtransformGroupにjsを指定します。
Style Dictionaryでは、このようにtransformGroupまたはtransformを指定することができます。
transformとは、デザイントークンを特定のプラットフォームに合わせて変換するための関数です。
configであらかじめ用意されているtransformを指定すると、その仕様に基づいて変換がされます。
transformGroupというのは、複数のtransformの組み合わせとして用意されているものです。
パスカルケースやスネークケース、RGBやHEXなど変換結果を細かく自分で指定したい場合は、transformGroupではなくtransformを一つ一つ選んで設定していきます。
(transformGroupをjsにした際に、透明色のHEX8桁のカラーコードが6桁になってしまったのですが、これはjsのカラーコードの仕様がcolor/hexだったからでした。この場合、transformにcolor/hex8を指定する必要があります。)
詳しくは以下を参考にしてください。
https://zenn.dev/helloiamktn/articles/db896cae14003f
https://amzn.github.io/style-dictionary/#/transforms
https://amzn.github.io/style-dictionary/#/transform_groups

buildPathにはビルド先のフォルダを指定します。
filesには、formatdestinationを複数記述できます。
今回はformatjavascript/es6を指定していますが、その他にもいくつか種類があります。自由にカスタムすることも可能です。https://amzn.github.io/style-dictionary/#/formats
destinationには、出力するファイル名を記載します。

package.jsonにscriptを追加します。

package.json
{
  ...
  "scripts": {
    "transform": "token-transformer tokens/figma-tokens.json tokens/style-dictionary-tokens.json",
    "build": "style-dictionary build --config style-dictionary.config.json",
  },
  ...
}

以下のコマンドを実行することで、buildフォルダの下にファイルがデザイントークンのtsファイルが作成されます。

yarn build

後は生成されたファイルをプロジェクトで使うだけです。
今回は以下のように出力されています。

design-tokens.ts
design-tokens.ts
/**
 * Do not edit directly
 * Generated on Sat, 18 Feb 2023 06:10:01 GMT
 */

export const TokenSetOrder0 = "theme";
export const ColorLightPrimaryLight = "#edd6ff";
export const ColorLightPrimaryMain = "#deb5ff";
export const ColorLightPrimaryDark = "#a56ed1";
export const ColorLightPrimaryDisabled = "#f3e2ff";
export const ColorLightSecondaryLight = "#d9ffdc";
export const ColorLightSecondaryMain = "#b7ffbe";
export const ColorLightSecondaryDark = "#7ce186";
export const ColorLightSecondaryDisabled = "#e2fde5";
export const ColorLightErrorLight = "#ffd0d0";
export const ColorLightErrorMain = "#ffa4a4";
export const ColorLightErrorDark = "#e06b6b";
export const ColorLightErrorDisabled = "#fddddf";
export const ColorLightNomalMain = "#e9e9e9";
export const ColorLightNomalHover = "#bbbbbb";
export const ColorLightTextMain = "#000000";
export const ColorLightTextDisabled = "#a9a9a9";
export const ColorLightTextWhite = "#fdfdfd";
export const ColorLightBackgroundMain = "#e8dfd2";
export const ColorLightBackgroundWhite = "#ffffff";
export const ColorLightBorderMain = "#000000";
export const ColorLightGrey100 = "#e9e9e9";
export const ColorLightGrey400 = "#e9e9e9";
export const ColorDarkPrimaryLight = "#b799cf";
export const ColorDarkPrimaryMain = "#aa80cc";
export const ColorDarkPrimaryDark = "#774b9a";
export const ColorDarkPrimaryDisabled = "#f3e2ff";
export const ColorDarkSecondaryLight = "#bce3c0";
export const ColorDarkSecondaryMain = "#92c897";
export const ColorDarkSecondaryDark = "#599960";
export const ColorDarkSecondaryDisabled = "#e2fde5";
export const ColorDarkErrorLight = "#d8afaf";
export const ColorDarkErrorMain = "#cd9191";
export const ColorDarkErrorDark = "#bd6161";
export const ColorDarkErrorDisabled = "#fddddf";
export const ColorDarkNomalMain = "#959595";
export const ColorDarkNomalHover = "#d7d7d7";
export const ColorDarkTextMain = "#ffffff";
export const ColorDarkTextDisabled = "#a9a9a9";
export const ColorDarkTextBlack = "#000000";
export const ColorDarkBackgroundMain = "#505050";
export const ColorDarkBackgroundWhite = "#ffffff";
export const ColorDarkBackgroundBlack = "#000000";
export const ColorDarkBorderMain = "#000000";
export const ColorDarkGrey100 = "#e9e9e9";
export const ColorDarkGrey200 = "#bfbfbf";
export const ColorDarkGrey300 = "#a5a5a5";
export const ColorDarkGrey500 = "#666666";
export const SpacingSmall = "8px";
export const SpacingMedium = "16px";
export const SpacingLarge = "24px";
export const SpacingXl = "32px";
export const SpacingXxl = "40px";
export const SpacingNone = 0;
export const ShadowSmall = {"x":3,"y":3,"blur":0,"spread":0,"color":"#000000","type":"dropShadow"};
export const ShadowMedeium = {"x":5,"y":5,"blur":0,"spread":0,"color":"#000000","type":"dropShadow"};
export const ShadowNone = {"x":0,"y":0,"blur":0,"spread":0,"color":"#000000","type":"dropShadow"};
export const FontSizeXxs = "10px";
export const FontSizeXs = "12px";
export const FontSizeSmall = "14px";
export const FontSizeMedium = "16px";
export const FontWeightMedium = "Medium";
export const FontWeightSemiBold = "Semi Bold";
export const FontWeightBold = "Bold";
export const FontFamilyInter = "Inter";
export const FontMediumMedium = {"fontFamily":"Inter","fontWeight":"Medium","fontSize":"16px"};
export const FontMediumSemiBold = {"fontFamily":"Inter","fontWeight":"Semi Bold","fontSize":"16px"};
export const FontMediumBold = {"fontFamily":"Inter","fontWeight":"Bold","fontSize":"16px"};
export const FontSmallMedium = {"fontFamily":"Inter","fontWeight":"Medium","fontSize":"14px"};
export const FontSmallSemiBold = {"fontFamily":"Inter","fontWeight":"Semi Bold","fontSize":"14px"};
export const FontSmallBold = {"fontFamily":"Inter","fontWeight":"Bold","fontSize":"14px"};
export const FontXsMedium = {"fontFamily":"Inter","fontSize":"12px","fontWeight":"Medium"};
export const FontXsSemiBold = {"fontFamily":"Inter","fontWeight":"Semi Bold","fontSize":"12px"};
export const FontXsBold = {"fontFamily":"Inter","fontWeight":"Bold","fontSize":"12px"};
export const FontXxsMedium = {"fontFamily":"Inter","fontWeight":"Medium","fontSize":"10px"};
export const FontXxsSemiBold = {"fontFamily":"Inter","fontWeight":"Semi Bold","fontSize":"10px"};
export const FontXxsBold = {"fontFamily":"Inter","fontWeight":"Bold","fontSize":"10px"};

生成されたファイルをプロジェクトのリポジトリにコピーするのが面倒なので、Style Dictionary用のリポジトリにコピー用のscriptを作成しておくと楽です。

最終的にpackage.jsonのscriptは以下のようになっています。

package.json
{
  ...
  "scripts": {
    "tokens": "yarn transform && yarn build",
    "build": "style-dictionary build --config style-dictionary.config.json",
    "transform": "token-transformer tokens/figma-tokens.json tokens/style-dictionary-tokens.json",
    "copy": "ts-node ./copy --project project-name"
  },
  ...
}

Stitchesのthemeに適用

ここからは、プロジェクトのリポジトリで作業します。
今回初めてStitchesを使ってみました。
https://stitches.dev/

Stitchesをインストールし、先ほど作成したdesign-tokens.tsをimportしてthemeを作成します。

theme.ts
theme.ts
import * as tokens from '@/design-tokens/design-tokens';
import { createStitches, createTheme } from '@stitches/react';

const { theme, styled } = createStitches({
  media: {
    bp1: '(min-width: 640px)',
  },
  theme: {
    primary: {
      light: tokens.ColorLightPrimaryLight,
      main: tokens.ColorLightPrimaryMain,
      dark: tokens.ColorLightPrimaryDark,
      disabled: tokens.ColorLightPrimaryDisabled,
    },
    secondary: {
      light: tokens.ColorLightSecondaryLight,
      main: tokens.ColorLightSecondaryMain,
      dark: tokens.ColorLightSecondaryDark,
      disabled: tokens.ColorLightSecondaryDisabled,
    },
    error: {
      light: tokens.ColorLightErrorLight,
      main: tokens.ColorLightErrorMain,
      dark: tokens.ColorLightErrorDark,
      disabled: tokens.ColorLightErrorDisabled,
    },
    grey: {
      grey100: tokens.ColorLightGrey100,
      grey400: tokens.ColorLightGrey400,
    },
    nomal: {
      main: tokens.ColorLightNomalMain,
      hover: tokens.ColorLightNomalHover,
    },
    text: {
      main: tokens.ColorLightTextMain,
      white: tokens.ColorLightTextWhite,
      disabled: tokens.ColorLightTextDisabled,
    },
    background: {
      main: tokens.ColorLightBackgroundMain,
      white: tokens.ColorLightBackgroundWhite,
    },
    border: {
      main: tokens.ColorLightBorderMain,
    },
    fontSize: {
      xxs: tokens.FontSizeXxs,
      xs: tokens.FontSizeXs,
      small: tokens.FontSizeSmall,
      medium: tokens.FontSizeMedium,
    },
    fontWeigt: {
      medium: tokens.FontWeightMedium,
      semiBold: tokens.FontWeightSemiBold,
      bold: tokens.FontWeightBold,
    },
    spacing: {
      small: tokens.SpacingSmall,
      medium: tokens.SpacingMedium,
      large: tokens.SpacingLarge,
      xl: tokens.SpacingXl,
      xxl: tokens.SpacingXxl,
    },
    shadow: {},
    fontSmallMedium: tokens.FontSmallMedium,
    fontSmallSemiBold: tokens.FontSmallSemiBold,
    fontSmallBold: tokens.FontSmallBold,
    fontMediumMedium: tokens.FontMediumMedium,
    fontMediumSemiBold: tokens.FontMediumSemiBold,
    fontMediumBold: tokens.FontMediumBold,
    fontXsMedium: tokens.FontXsMedium,
    fontXsSemiBold: tokens.FontXsSemiBold,
    fontXsBold: tokens.FontXsBold,
    fontXxsMedium: tokens.FontXxsMedium,
    fontXxsSemiBold: tokens.FontXxsSemiBold,
    fontXxsBold: tokens.FontXxsBold,
  },
});

const darkTheme = createTheme({
  primary: {
    light: tokens.ColorDarkPrimaryLight,
    main: tokens.ColorDarkPrimaryMain,
    dark: tokens.ColorDarkPrimaryDark,
    disabled: tokens.ColorDarkPrimaryDisabled,
  },
  secondary: {
    light: tokens.ColorDarkSecondaryLight,
    main: tokens.ColorDarkSecondaryMain,
    dark: tokens.ColorDarkSecondaryDark,
    disabled: tokens.ColorDarkSecondaryDisabled,
  },
  error: {
    light: tokens.ColorDarkErrorLight,
    main: tokens.ColorDarkErrorMain,
    dark: tokens.ColorDarkErrorDark,
    disabled: tokens.ColorDarkErrorDisabled,
  },
  grey: {
    grey100: tokens.ColorDarkGrey100,
    grey200: tokens.ColorDarkGrey200,
    grey300: tokens.ColorDarkGrey300,
    grey500: tokens.ColorDarkGrey500,
  },
  nomal: {
    main: tokens.ColorDarkNomalMain,
    hover: tokens.ColorDarkNomalHover,
  },
  text: {
    main: tokens.ColorDarkTextMain,
    black: tokens.ColorDarkTextBlack,
    disabled: tokens.ColorDarkTextDisabled,
  },
  background: {
    main: tokens.ColorDarkBackgroundMain,
    white: tokens.ColorDarkBackgroundWhite,
    black: tokens.ColorDarkBackgroundBlack,
  },
  border: {
    main: tokens.ColorDarkBorderMain,
  },
});

export { theme, darkTheme, styled };

コンポーネントで使用

後はコンポーネントでthemeやstyledをimportするだけです。

Button.tsx
import React from 'react';
import type * as Stitches from '@stitches/react';
import { theme, styled } from '@/utils/theme';
import { PlusIcon } from '@radix-ui/react-icons';

type Props = {
  label: string;
  variant: Stitches.VariantProps<typeof ButtonBase>['variant'];
  disabled?: boolean;
  icon?: boolean;
};

const ButtonBase = styled('button', {
  ...theme.fontMediumBold,
  border: '3px solid',
  height: '48px',
  paddingRight: theme.spacing.large,
  paddingLeft: theme.spacing.large,
  borderRadius: '30px',
  boxShadow: '3px 3px',
  color: theme.text.main,
  borderColor: theme.border.main,
  '& svg': {
    marginRight: '5px',
    stroke: theme.text.main,
  },
  '&:disabled': {
    borderColor: theme.text.disabled,
    color: theme.text.disabled,
    boxShadow: 'none',
    '& svg': {
      stroke: theme.text.disabled,
    },
  },
  variants: {
    width: {
      desktop: { width: 'inherit' },
      sp: { width: '100%' },
    },
    variant: {
      primary: {
        backgroundColor: theme.primary.main,
        '&:hover': {
          backgroundColor: theme.primary.light,
        },
        '&:disabled': {
          backgroundColor: theme.primary.disabled,
        },
      },
      secondary: {
        backgroundColor: theme.secondary.main,
        '&:hover': {
          backgroundColor: theme.secondary.light,
        },
        '&:disabled': {
          backgroundColor: theme.secondary.disabled,
        },
      },
      error: {
        backgroundColor: theme.error.main,
        '&:hover': {
          backgroundColor: theme.error.light,
        },
        '&:disabled': {
          backgroundColor: theme.error.disabled,
        },
      },
    },
  },
});

export const Button: React.FC<Props> = ({ label, variant, disabled, icon }) => {
  return (
    <ButtonBase
      variant={variant}
      disabled={disabled}
      width={{ '@initial': 'sp', '@bp1': 'desktop' }}
    >
      {icon && <PlusIcon />}
      {label}
    </ButtonBase>
  );
};

Storybookで見るとこんな感じです。

おわりに 😪

theme.tsも自動で作れたら最高なんだけどな〜と思っています。たぶんformat用のテンプレートを作成すればできるけど頑張れなかったです。もしも頑張れたら追記します。
FigmaからデザイントークンをStyle Dictionary用のリポジトリにpushした時に、GitHub Actionsを使って自動でbuildしたファイルをプロジェクトのリポジトリにPRとして出すこともやってみたいので、こちらもできたら追記します。

Discussion