T-CREATOR

Storybook × Design Tokens 設計:Style Dictionary とテーマ切替の連携

Storybook × Design Tokens 設計:Style Dictionary とテーマ切替の連携

デザインシステムの構築において、一貫性のある UI を実現するためには、色やフォント、スペーシングなどのデザイン要素を体系的に管理することが重要です。

近年、Design Tokens という概念が注目を集めており、デザインの変数を一元管理することで、複数のプラットフォームやフレームワークで統一されたデザインを実現できるようになりました。さらに、Storybook と Style Dictionary を組み合わせることで、デザイントークンの管理と UI コンポーネントのプレビューを効率的に行えます。

本記事では、Storybook における Design Tokens 設計の基礎から、Style Dictionary を用いた実装、そしてテーマ切替の連携方法まで、段階的に解説していきます。

背景

Design Tokens の誕生

Design Tokens は、Salesforce のデザインシステム「Lightning Design System」で初めて提唱された概念です。デザインの基本要素(色、タイポグラフィ、スペーシング、影など)を変数として定義し、プラットフォームに依存しない形式で管理する手法ですね。

従来、デザインの値はデザイナーが Figma や Sketch で定義し、開発者がそれをコードに手動で転記していました。この方法では、デザインの更新が発生するたびに、手作業での変更が必要となり、ミスや不整合が発生しやすい状況でした。

以下の図は、Design Tokens を導入する前後のワークフローの違いを示しています。

mermaidflowchart LR
    designer["デザイナー<br/>Figma/Sketch"] -->|手動転記| dev["開発者<br/>CSS/SCSS"]
    dev -->|実装| web["Webアプリ"]
    dev -->|実装| mobile["モバイルアプリ"]

    style designer fill:#e1f5ff
    style dev fill:#fff4e1
    style web fill:#f0f0f0
    style mobile fill:#f0f0f0

上図では、デザイナーから開発者への情報伝達が手動で行われるため、エラーが発生しやすい構造になっていることがわかります。

Style Dictionary の役割

Style Dictionary は、Amazon が開発したオープンソースのツールで、Design Tokens を様々なプラットフォーム向けの形式に変換できます。JSON 形式で定義されたトークンを、CSS、SCSS、JavaScript、iOS、Android など、異なるプラットフォームで利用可能な形式に自動変換してくれるのです。

Design Tokens を中心としたワークフローでは、以下のような流れになります。

mermaidflowchart TB
    tokens["Design Tokens<br/>JSON形式"] -->|変換| sd["Style Dictionary"]
    sd -->|CSS変数| css["CSS/SCSS"]
    sd -->|JS Object| js["JavaScript/TypeScript"]
    sd -->|Swift| ios["iOS"]
    sd -->|XML| android["Android"]

    css --> web["Webアプリ"]
    js --> web
    ios --> mobileApp["モバイルアプリ"]
    android --> mobileApp

    style tokens fill:#e8f5e9
    style sd fill:#fff9c4
    style css fill:#e1f5ff
    style js fill:#e1f5ff
    style ios fill:#ffe1f5
    style android fill:#ffe1f5

この図が示すように、単一のソース(Design Tokens)から複数のプラットフォーム向けの出力を生成できるため、一貫性が保たれます。

Storybook との連携意義

Storybook は、UI コンポーネントを独立した環境で開発・テストするためのツールです。Design Tokens と Storybook を連携させることで、デザインシステム全体を可視化し、デザイナーと開発者が同じ画面を見ながらコミュニケーションできる環境が整います。

課題

従来のデザイン管理の問題点

Design Tokens や Style Dictionary を導入していない環境では、以下のような課題が発生しやすくなります。

#課題具体例影響
1値の不整合デザインツールでは #2196F3 だが、コードでは #2195F3ブランドカラーのばらつき
2更新の手間テーマカラー変更時に数百ファイルを修正工数増加、ミスの発生
3プラットフォーム間の差異Web とモバイルでフォントサイズがずれるユーザー体験の不統一
4ドキュメント不足どの色をどこで使うか不明瞭デザインガイドライン違反
5テーマ切替の複雑さダークモード実装が困難機能追加のハードルが高い

これらの課題は、組織の規模が大きくなるほど深刻化します。デザインシステムを持つ企業では、数十から数百のコンポーネントを管理する必要があり、手動での管理は現実的ではありません。

テーマ切替における技術的課題

特にテーマ切替(ライトモード・ダークモード)の実装では、以下のような技術的な課題があります。

mermaidflowchart TD
    start["テーマ切替の実装"] --> decision{"実装方法"}
    decision -->|パターン1| hardcode["ハードコード"]
    decision -->|パターン2| cssvar["CSS変数"]
    decision -->|パターン3| tokens["Design Tokens"]

    hardcode --> problem1["コンポーネントごとに<br/>条件分岐が必要"]
    cssvar --> problem2["命名規則の統一が困難"]
    tokens --> solution["一元管理で<br/>自動切替"]

    problem1 --> bad["保守性が低い"]
    problem2 --> medium["中程度の保守性"]
    solution --> good["高い保守性"]

    style hardcode fill:#ffcdd2
    style cssvar fill:#fff9c4
    style tokens fill:#c8e6c9
    style bad fill:#ffcdd2
    style medium fill:#fff9c4
    style good fill:#c8e6c9

上図のように、実装方法によって保守性が大きく変わります。Design Tokens を活用することで、テーマの定義を一箇所に集約し、自動的に全コンポーネントへ反映できるのです。

Storybook でのプレビュー課題

Storybook を使用する際も、テーマ切替のプレビューには工夫が必要でした。

複数のテーマを Storybook で確認する場合、従来は各 Story にテーマごとのバリエーションを手動で記述する必要がありました。これでは、新しいテーマを追加するたびに全ての Story を更新しなければならず、非効率的ですね。

解決策

Design Tokens + Style Dictionary + Storybook の統合アーキテクチャ

これらの課題を解決するために、Design Tokens、Style Dictionary、Storybook を統合したアーキテクチャを構築します。

以下の図は、全体のアーキテクチャを示しています。

mermaidflowchart TB
    subgraph design["デザインフェーズ"]
        figma["Figma<br/>デザインツール"]
    end

    subgraph tokens_layer["Design Tokensレイヤー"]
        base["base.json<br/>基本トークン"]
        light["light.json<br/>ライトテーマ"]
        dark["dark.json<br/>ダークテーマ"]
    end

    subgraph build["ビルドフェーズ"]
        sd["Style Dictionary<br/>変換処理"]
    end

    subgraph output["出力"]
        css_vars["CSS変数"]
        js_tokens["JavaScriptトークン"]
        ts_types["TypeScript型定義"]
    end

    subgraph app["アプリケーション"]
        components["Reactコンポーネント"]
        storybook["Storybook"]
    end

    figma -.->|エクスポート| base
    base --> sd
    light --> sd
    dark --> sd

    sd --> css_vars
    sd --> js_tokens
    sd --> ts_types

    css_vars --> components
    js_tokens --> components
    ts_types --> components

    components --> storybook

    style figma fill:#a5d6a7
    style sd fill:#fff59d
    style storybook fill:#90caf9

この統合アーキテクチャにより、デザインからコードまでの一貫したフローが実現できます。

Style Dictionary の設定方針

Style Dictionary は設定ファイルで動作をカスタマイズできます。主な設定項目は以下の通りです。

#設定項目説明
1sourceトークンファイルの場所tokens​/​**​/​*.json
2platforms出力プラットフォームweb, ios, android
3transforms変換ルールname​/​cti​/​kebab, color​/​hex
4format出力形式css​/​variables, javascript​/​es6
5buildPath出力先パスbuild​/​css​/​, build​/​js​/​

これらの設定により、柔軟な変換処理が可能になります。

Storybook のテーマ切替戦略

Storybook でテーマ切替を実現するには、いくつかのアプローチがあります。

推奨する方法は、Storybook のglobalTypesdecoratorsを活用する方法です。これにより、ツールバーにテーマ切替ボタンを追加し、全ての Story で自動的にテーマを切り替えられるようになります。

mermaidsequenceDiagram
    participant User as ユーザー
    participant Toolbar as Storybookツールバー
    participant Decorator as グローバルDecorator
    participant Story as Storyコンポーネント

    User->>Toolbar: テーマ切替ボタンクリック
    Toolbar->>Decorator: テーマ変更イベント
    Decorator->>Decorator: CSS変数を更新
    Decorator->>Story: 再レンダリング
    Story->>User: 新しいテーマで表示

上記のシーケンス図が示すように、ユーザーの操作からコンポーネントの再描画まで、自動的に処理が流れていきます。

具体例

プロジェクト構造

まず、推奨するプロジェクト構造を示します。

csharpproject-root/
├── tokens/              # Design Tokensディレクトリ
│   ├── base.json       # 基本トークン
│   ├── light.json      # ライトテーマ
│   └── dark.json       # ダークテーマ
├── config/
│   └── style-dictionary.config.js  # Style Dictionary設定
├── build/              # 生成ファイル(git管理外)
│   ├── css/
│   └── js/
├── src/
│   ├── components/     # Reactコンポーネント
│   └── styles/
└── .storybook/         # Storybook設定
    ├── preview.js
    └── manager.js

この構造により、トークンの定義、変換、利用が明確に分離されます。

Step 1: Design Tokens の定義

最初に、基本となる Design Tokens を定義します。JSON ファイルで、階層的にトークンを管理していきましょう。

基本トークン(tokens/base.json)の定義

json{
  "color": {
    "base": {
      "gray": {
        "100": { "value": "#f7fafc" },
        "200": { "value": "#edf2f7" },
        "300": { "value": "#e2e8f0" },
        "400": { "value": "#cbd5e0" },
        "500": { "value": "#a0aec0" },
        "600": { "value": "#718096" },
        "700": { "value": "#4a5568" },
        "800": { "value": "#2d3748" },
        "900": { "value": "#1a202c" }
      }
    }
  }
}

基本トークンでは、グレースケールなど、テーマに依存しない基本的な色を定義します。

ライトテーマトークン(tokens/light.json)の定義

json{
  "color": {
    "background": {
      "primary": { "value": "{color.base.gray.100.value}" },
      "secondary": {
        "value": "{color.base.gray.200.value}"
      }
    },
    "text": {
      "primary": { "value": "{color.base.gray.900.value}" },
      "secondary": {
        "value": "{color.base.gray.700.value}"
      }
    },
    "border": {
      "default": { "value": "{color.base.gray.300.value}" }
    }
  }
}

ライトテーマでは、セマンティックな名前(background、text など)に基本トークンを参照する形で定義します。これにより、目的が明確になるのです。

ダークテーマトークン(tokens/dark.json)の定義

json{
  "color": {
    "background": {
      "primary": { "value": "{color.base.gray.900.value}" },
      "secondary": {
        "value": "{color.base.gray.800.value}"
      }
    },
    "text": {
      "primary": { "value": "{color.base.gray.100.value}" },
      "secondary": {
        "value": "{color.base.gray.300.value}"
      }
    },
    "border": {
      "default": { "value": "{color.base.gray.700.value}" }
    }
  }
}

ダークテーマは同じ構造で、値のみを反転させています。この一貫性が重要ですね。

Step 2: Style Dictionary の設定

次に、Style Dictionary の設定ファイルを作成し、トークンを CSS 変数や JavaScript オブジェクトに変換する設定を行います。

パッケージのインストール

bashyarn add -D style-dictionary

Style Dictionary 設定ファイル(config/style-dictionary.config.js)

javascriptmodule.exports = {
  source: ['tokens/base.json'],
  platforms: {
    css: {
      transformGroup: 'css',
      buildPath: 'build/css/',
      files: [
        {
          destination: 'variables.css',
          format: 'css/variables',
        },
      ],
    },
  },
};

この基本設定では、tokens​/​base.jsonを読み込み、CSS 変数形式で出力します。

テーマ別の設定を追加

複数テーマに対応するために、設定を拡張します。

javascriptmodule.exports = {
  source: ['tokens/base.json'],
  platforms: {
    'css-light': {
      transformGroup: 'css',
      buildPath: 'build/css/',
      files: [
        {
          destination: 'theme-light.css',
          format: 'css/variables',
          options: {
            selector: '[data-theme="light"]',
          },
          filter: (token) =>
            token.filePath.includes('light.json'),
        },
      ],
    },
  },
};

このコードでは、data-theme="light"属性を持つ要素にスタイルが適用されるよう、セレクタを指定しています。

完全な設定ファイル

javascriptmodule.exports = {
  source: ['tokens/**/*.json'],
  platforms: {
    css: {
      transformGroup: 'css',
      buildPath: 'build/css/',
      files: [
        {
          destination: 'theme-light.css',
          format: 'css/variables',
          options: {
            selector: '[data-theme="light"]',
          },
          filter: (token) => {
            return (
              token.filePath.includes('light.json') ||
              token.filePath.includes('base.json')
            );
          },
        },
        {
          destination: 'theme-dark.css',
          format: 'css/variables',
          options: {
            selector: '[data-theme="dark"]',
          },
          filter: (token) => {
            return (
              token.filePath.includes('dark.json') ||
              token.filePath.includes('base.json')
            );
          },
        },
      ],
    },
  },
};

この設定により、ライトテーマとダークテーマの両方の CSS 変数が生成されます。

JavaScript 出力の追加

TypeScript でトークンを使用するために、JavaScript 形式の出力も追加しましょう。

javascriptplatforms: {
  // ... CSS設定は省略
  js: {
    transformGroup: 'js',
    buildPath: 'build/js/',
    files: [
      {
        destination: 'tokens.js',
        format: 'javascript/es6'
      }
    ]
  }
}

ビルドスクリプトの追加(package.json)

json{
  "scripts": {
    "tokens:build": "style-dictionary build --config config/style-dictionary.config.js",
    "tokens:watch": "nodemon --watch tokens --exec yarn tokens:build"
  }
}

これにより、yarn tokens:buildでトークンをビルドできます。

Step 3: 生成された CSS 変数の確認

Style Dictionary を実行すると、以下のような CSS 変数が生成されます。

生成された CSS(build/css/theme-light.css)

css[data-theme='light'] {
  --color-base-gray-100: #f7fafc;
  --color-base-gray-200: #edf2f7;
  --color-base-gray-300: #e2e8f0;
  --color-base-gray-900: #1a202c;
  --color-background-primary: #f7fafc;
  --color-background-secondary: #edf2f7;
  --color-text-primary: #1a202c;
  --color-text-secondary: #4a5568;
  --color-border-default: #e2e8f0;
}

生成された CSS(build/css/theme-dark.css)

css[data-theme='dark'] {
  --color-base-gray-100: #f7fafc;
  --color-base-gray-900: #1a202c;
  --color-background-primary: #1a202c;
  --color-background-secondary: #2d3748;
  --color-text-primary: #f7fafc;
  --color-text-secondary: #e2e8f0;
  --color-border-default: #4a5568;
}

これらの CSS 変数を、コンポーネントのスタイルで使用できるようになります。

Step 4: React コンポーネントでの利用

生成された CSS 変数を、React コンポーネントで利用します。

コンポーネントのスタイル定義(src/components/Button/Button.module.css)

css.button {
  background-color: var(--color-background-secondary);
  color: var(--color-text-primary);
  border: 1px solid var(--color-border-default);
  padding: 0.5rem 1rem;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.2s;
}

CSS 変数を使用することで、テーマが切り替わると自動的にスタイルが更新されます。

Button コンポーネント(src/components/Button/Button.tsx)

typescriptimport React from 'react';
import styles from './Button.module.css';

interface ButtonProps {
  children: React.ReactNode;
  onClick?: () => void;
  variant?: 'primary' | 'secondary';
}

export const Button: React.FC<ButtonProps> = ({
  children,
  onClick,
  variant = 'primary',
}) => {
  return (
    <button className={styles.button} onClick={onClick}>
      {children}
    </button>
  );
};

コンポーネント自体はテーマを意識せず、CSS 変数を通じて自動的にテーマが適用されるのです。

テーマプロバイダーの実装(src/components/ThemeProvider/ThemeProvider.tsx)

typescriptimport React, {
  createContext,
  useContext,
  useState,
} from 'react';

type Theme = 'light' | 'dark';

interface ThemeContextType {
  theme: Theme;
  setTheme: (theme: Theme) => void;
}

const ThemeContext = createContext<
  ThemeContextType | undefined
>(undefined);

まず、Context を使用してテーマ状態を管理するための型定義を行います。

ThemeProvider コンポーネントの実装

typescriptexport const ThemeProvider: React.FC<{
  children: React.ReactNode;
}> = ({ children }) => {
  const [theme, setTheme] = useState<Theme>('light');

  React.useEffect(() => {
    // HTMLのdata-theme属性を更新
    document.documentElement.setAttribute(
      'data-theme',
      theme
    );
  }, [theme]);

  return (
    <ThemeContext.Provider value={{ theme, setTheme }}>
      {children}
    </ThemeContext.Provider>
  );
};

この Provider コンポーネントは、テーマが変更されるたびにdata-theme属性を更新し、CSS 変数が自動的に切り替わるようにします。

カスタムフックの実装

typescriptexport const useTheme = () => {
  const context = useContext(ThemeContext);
  if (!context) {
    throw new Error(
      'useTheme must be used within ThemeProvider'
    );
  }
  return context;
};

このフックにより、どのコンポーネントからでも簡単にテーマにアクセスできます。

Step 5: Storybook の設定

次に、Storybook でテーマ切替を実現する設定を行います。

CSS 変数のインポート(.storybook/preview.js)

javascriptimport '../build/css/theme-light.css';
import '../build/css/theme-dark.css';

export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

まず、生成されたテーマ CSS をインポートします。

グローバルタイプの定義

javascriptexport const globalTypes = {
  theme: {
    name: 'Theme',
    description: 'Global theme for components',
    defaultValue: 'light',
    toolbar: {
      icon: 'circlehollow',
      items: [
        { value: 'light', title: 'Light', icon: 'sun' },
        { value: 'dark', title: 'Dark', icon: 'moon' },
      ],
      showName: true,
      dynamicTitle: true,
    },
  },
};

この設定により、Storybook のツールバーにテーマ切替ボタンが表示されます。

グローバル Decorator の実装

javascriptimport React, { useEffect } from 'react';

const withTheme = (Story, context) => {
  const theme = context.globals.theme || 'light';

  useEffect(() => {
    document.documentElement.setAttribute(
      'data-theme',
      theme
    );
  }, [theme]);

  return <Story {...context} />;
};

export const decorators = [withTheme];

Decorator を使用することで、全ての Story に自動的にテーマ切替機能が適用されます。これにより、各 Story 個別にテーマ対応を実装する必要がなくなるのです。

完全な.storybook/preview.js

javascriptimport React, { useEffect } from 'react';
import '../build/css/theme-light.css';
import '../build/css/theme-dark.css';

export const globalTypes = {
  theme: {
    name: 'Theme',
    description: 'Global theme for components',
    defaultValue: 'light',
    toolbar: {
      icon: 'circlehollow',
      items: [
        { value: 'light', title: 'Light', icon: 'sun' },
        { value: 'dark', title: 'Dark', icon: 'moon' },
      ],
      showName: true,
    },
  },
};

Decorator 関数の完全版

javascriptconst withTheme = (Story, context) => {
  const theme = context.globals.theme || 'light';

  useEffect(() => {
    document.documentElement.setAttribute(
      'data-theme',
      theme
    );
  }, [theme]);

  return (
    <div
      style={{
        minHeight: '100vh',
        backgroundColor: 'var(--color-background-primary)',
        padding: '2rem',
      }}
    >
      <Story {...context} />
    </div>
  );
};

export const decorators = [withTheme];

背景色も CSS 変数を使用することで、Story のプレビュー全体がテーマに対応します。

Step 6: Story の作成

最後に、実際の Story を作成してテーマ切替を確認しましょう。

Button コンポーネントのストーリー(src/components/Button/Button.stories.tsx)

typescriptimport React from 'react';
import {
  ComponentStory,
  ComponentMeta,
} from '@storybook/react';
import { Button } from './Button';

export default {
  title: 'Components/Button',
  component: Button,
  argTypes: {
    children: { control: 'text' },
  },
} as ComponentMeta<typeof Button>;

基本的な Story の設定を行います。

Template の定義とストーリーの作成

typescriptconst Template: ComponentStory<typeof Button> = (args) => (
  <Button {...args} />
);

export const Primary = Template.bind({});
Primary.args = {
  children: 'Primary Button',
  variant: 'primary',
};

export const Secondary = Template.bind({});
Secondary.args = {
  children: 'Secondary Button',
  variant: 'secondary',
};

各 Story は、グローバル Decorator によって自動的にテーマ切替に対応します。ツールバーのテーマボタンをクリックするだけで、全てのコンポーネントがテーマに応じた見た目に切り替わるのです。

動作確認とデバッグ

実装が完了したら、以下の手順で動作を確認します。

ビルドと Storybook の起動

bash# Design Tokensをビルド
yarn tokens:build

# Storybookを起動
yarn storybook

確認ポイント

#確認項目期待される動作
1CSS 変数の生成build​/​css​/​に各テーマの CSS ファイルが生成される
2ツールバー表示Storybook にテーマ切替ボタンが表示される
3テーマ切替ボタンクリックでコンポーネントの色が変わる
4data-theme 属性HTML のdata-theme属性が正しく更新される
5背景色の変化プレビュー全体の背景色もテーマに応じて変わる

デバッグ時は、ブラウザの開発者ツールで CSS 変数の値を確認すると良いでしょう。

応用例:カスタムトークンの追加

基本的な実装ができたら、さらに詳細なトークンを追加できます。

フォントトークンの追加(tokens/base.json)

json{
  "font": {
    "size": {
      "xs": { "value": "0.75rem" },
      "sm": { "value": "0.875rem" },
      "base": { "value": "1rem" },
      "lg": { "value": "1.125rem" },
      "xl": { "value": "1.25rem" },
      "2xl": { "value": "1.5rem" }
    },
    "weight": {
      "normal": { "value": "400" },
      "medium": { "value": "500" },
      "semibold": { "value": "600" },
      "bold": { "value": "700" }
    }
  }
}

スペーシングトークンの追加

json{
  "spacing": {
    "xs": { "value": "0.25rem" },
    "sm": { "value": "0.5rem" },
    "md": { "value": "1rem" },
    "lg": { "value": "1.5rem" },
    "xl": { "value": "2rem" },
    "2xl": { "value": "3rem" }
  }
}

これらのトークンも同様にビルドすることで、CSS 変数として利用可能になります。

まとめ

本記事では、Storybook における Design Tokens 設計の実践方法について解説しました。

Design Tokens は、デザインの基本要素を変数として一元管理する概念で、Style Dictionary を使用することで、複数のプラットフォーム向けに自動変換できます。Storybook と組み合わせることで、デザインシステム全体を可視化し、テーマ切替も簡単に実装できるようになりました。

実装のポイントをまとめると、以下の通りです。

重要なポイント

#ポイント説明
1トークンの階層化base → セマンティック名の 2 層構造で管理
2Style Dictionary の活用JSON から各プラットフォーム用ファイルを自動生成
3CSS 変数の使用data-theme属性で動的にテーマを切替
4Storybook のグローバル設定Decorator で全 Story 自動対応
5一貫性の維持単一のソースから複数出力で不整合を防止

この手法を導入することで、デザイナーと開発者の協業がスムーズになり、ブランドの一貫性を保ちながら、効率的に UI コンポーネントを開発できるでしょう。

さらに、新しいテーマ(例:ハイコントラストモード、カスタムブランドテーマ)を追加する際も、JSON ファイルを 1 つ追加するだけで、全コンポーネントに自動的に適用される仕組みが構築できています。

Design Tokens を中心としたデザインシステムは、組織の成長に合わせてスケールする強力な基盤となります。ぜひ、皆さんのプロジェクトでも導入を検討してみてください。

関連リンク