Tokens Studio for Figma + Style Dictionaryを試してみた
はじめに 🏔
Tokens Studio for Figma(旧 Figma Tokens)で作成したデザイントークンをStyle Dictionaryで変換してみました。
Tokens StudioとStyle Dictionaryについての詳細は省きますが、ものすごく大まかに説明をすると、Tokens Studioは色やフォント等のデザイントークンを管理するためのFigmaプラグインで、Style DictionaryはiOSやWebなどの各プラットフォーム向けにデザイントークンを変換してくれるライブラリです。
詳細は以下ドキュメントを参考にしてください。
やってみよう 🫴🏻
おおまかな流れは以下です。
- 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というライブラリを使います。
token-transformer
をインストールします。
yarn add -D token-transformer
installしたら、package.jsonにscriptを追加します。
{
...
"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に追加していきます。
{
"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://amzn.github.io/style-dictionary/#/transforms
https://amzn.github.io/style-dictionary/#/transform_groups
buildPath
にはビルド先のフォルダを指定します。
files
には、format
とdestination
を複数記述できます。
今回はformat
にjavascript/es6
を指定していますが、その他にもいくつか種類があります。自由にカスタムすることも可能です。https://amzn.github.io/style-dictionary/#/formats
destination
には、出力するファイル名を記載します。
package.jsonにscriptを追加します。
{
...
"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
/**
* 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は以下のようになっています。
{
...
"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を使ってみました。
Stitchesをインストールし、先ほど作成したdesign-tokens.ts
をimportしてthemeを作成します。
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するだけです。
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