土曜日, 7月 26, 2025
土曜日, 7月 26, 2025
- Advertisment -
ホームニューステックニュースTypeScript でデザイントークン管理 ➜ Vite で CSS 変数を自動生成する方法

TypeScript でデザイントークン管理 ➜ Vite で CSS 変数を自動生成する方法



🌟 はじめに — 目的と背景

「デザイントークンをコードで一元管理したい」
これまでは私のプロジェクトでも、Figma 上で決めたカラーパレットや余白などを 共通の CSS 変数ファイル(variable.css) に直接記述して管理していました。
しかし、実際の開発では TypeScript 側にトークンとして定義されていなかったため、必要な値を使うたびに Figma や CSS ファイルを探して手動でコピペする という非効率な運用になっていました。

この記事では、TypeScript で定義したデザイントークンからカスタムプロパティ(CSS 変数)を自動生成し、Vite のビルド時に共通スタイルとして書き出す方法 をまとめます。


🎯 この記事の対象者

以下のような方に役立つ内容です。

  • デザイントークンを TypeScript で一元管理したい方
  • Figma のカラーパレットやサイズをコードに自動で同期したい方
  • module.css や CSS Modules でカスタムプロパティを使い回したい方
  • Vite プロジェクトでテーマを効率的に管理したい方

⚙️ 使用技術

  • TypeScript:デザイントークン管理
  • Vite:ビルドツール
  • Vite プラグイン:自作プラグインで CSS を自動生成
  • Node.js(fs:CSS ファイル出力

🎨 実現イメージ

  • tokens.ts に色・余白・サイズを定義
  • flattenTokens で階層構造をフラット化し、kebab-case に変換
  • Vite のビルド時に variable.css を自動生成
  • 生成された CSS は :root--color-primary のように出力
  • コンポーネントでは var(--color-primary) として利用可能

📂 フォルダ構成(例)

my-project/
├── src/
│   ├── tokens/
│   │   └── index.ts          # デザイントークン定義
│   ├── assets/
│   │   └── styles/
│   │       └── variable.css  # 自動生成される CSS
│   ├── App.tsx               # メインアプリ
│   └── ...                   # その他のソースコード
├── scripts/
│   ├── token-utils.ts        # flatten/kebab-case ユーティリティ
│   └── css-token-plugin.ts   # Vite プラグイン
├── vite.config.ts            # Vite 設定
├── package.json
└── tsconfig.json

✏️ サンプルコード

📌 ① デザイントークンの定義

Figma などで決めた 色・余白・ブレイクポイント などを、TypeScript のオブジェクトとしてまとめて管理します。


export const tokens = {
  color: {
    primary: "#0070f3",
    secondary: "#7928ca",
    white: "#fff",
    black: "#000",
  },
  spacing: {
    small: "4px",
    medium: "8px",
    large: "16px",
  },
};

🧰 ② ユーティリティ

デザイントークンを CSS カスタムプロパティに変換するためのユーティリティです。

📌 ポイント

  • キャメルケース → ケバブケース に変換し、CSS の命名規則に合わせる
  • ネストされたオブジェクトをフラット化し、--color-primary の形式に整形
  • Stylelint の custom-property-pattern に準拠しているため、すべてを kebab-case に揃える

私のプロジェクトでは Stylelint の custom-property-pattern を適用しているため、カスタムプロパティ名を kebab-case に揃える必要がありました。



export const toKebabCase = (str: string) =>
  str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();


export const isKebabCase = (str: string) => /^[a-z0-9-]+$/.test(str);


export const flattenTokens = (
  obj: Recordstring, unknown>,
  prefix = ""
): Recordstring, string> => {
  const result: Recordstring, string> = {};

  Object.entries(obj).forEach(([key, value]) => {
    
    const kebabKey = isKebabCase(key) ? key : toKebabCase(key);
    const newKey = prefix ? `${prefix}-${kebabKey}` : kebabKey;

    if (typeof value === "string" || typeof value === "number") {
      
      result[newKey] = String(value);
    } else if (typeof value === "object" && value !== null) {
      
      Object.assign(result, flattenTokens(value as Recordstring, unknown>, newKey));
    }
  });

  return result;
};

🔌 ③ Vite プラグイン

② のユーティリティを使い、ビルド時に CSS 変数を自動生成する Vite プラグイン を作成します。

📌 ポイント

  • TypeScript で定義したトークンを読み込む
  • flattenTokens で階層構造をフラット化し、ケバブケースに変換
  • :root に CSS カスタムプロパティとして書き出し、variable.css に保存

import { Plugin } from "vite";
import { writeFileSync } from "fs";
import { flattenTokens } from "./token-utils";
import { tokens } from "../src/tokens/index";

export const cssTokenPlugin = (): Plugin => ({
  name: "generate-css-tokens",
  buildStart() {
    const flatTokens = flattenTokens(tokens);
    const customProperty = Object.entries(flatTokens)
      .map(([key, val]) => `  --${key}: ${val};`)
      .join("\n");

    const css = `:root {\n${customProperty}\n}`;

    writeFileSync("./src/assets/styles/variable.css", css);
  },
});

⚙️ ④ Vite 設定

作成したプラグインを Vite に登録すれば、ビルド時に自動で CSS が生成されます。

以下は最もシンプルな例です。


import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
import { cssTokenPlugin } from "./scripts/css-token-plugin";

export default defineConfig({
  plugins: [react(), cssTokenPlugin()],
  resolve: {
    alias: {
      "@": path.resolve(__dirname, "src"),
    },
  },
});

❗️ このままだと困る点
この形だと、ビルドのたびに必ず CSS ファイルが上書きされるため、
内容が変わっていなくても無駄にファイル更新が発生します。
また、開発中に tokens.ts を更新しても自動では反映されません。

✅ そこで、より便利にする改良版
以下のようにすると、
差分がない場合はファイルを上書きしないようにし、
tokens.ts の変更を監視して、自動で CSS を再生成できるようになります。


import { Plugin } from "vite";
import { writeFileSync, existsSync, readFileSync } from "fs";
import path from "path";
import { flattenTokens } from "./token-utils";
import { tokens } from "../src/tokens/index";

const generateCss = () => {
  const flatTokens = flattenTokens(tokens);
  const customProperty = Object.entries(flatTokens)
    .map(([key, val]) => `  --${key}: ${val};`)
    .join("\n");
  return `:root {\n${customProperty}\n}`;
};

const writeIfChanged = (filePath: string, content: string) => {
  if (existsSync(filePath)) {
    const current = readFileSync(filePath, "utf-8");
    if (current === content) {
      console.log("✅ CSS tokens unchanged. Skip write.");
      return;
    }
  }
  writeFileSync(filePath, content);
  console.log(`✅ CSS tokens generated: ${filePath}`);
};

export const cssTokenPlugin = (): Plugin => {
  const tokenFilePath = path.resolve(__dirname, "../src/tokens/index.ts");
  const outputFilePath = path.resolve(__dirname, "../src/assets/styles/variable.css");

  return {
    name: "generate-css-tokens",
    buildStart() {
      const css = generateCss();
      writeIfChanged(outputFilePath, css);
      this.addWatchFile(tokenFilePath);
    },
    watchChange(id) {
      if (id === tokenFilePath) {
        console.log(`🔄 tokens.ts changed → regenerate CSS tokens`);
        const css = generateCss();
        writeIfChanged(outputFilePath, css);
      }
    },
  };
};

📄 ⑤ 出力される CSS の例

:root {
  --color-primary: #0070f3;
  --color-secondary: #7928ca;
  --color-white: #fff;
  --color-black: #000;
  --spacing-small: 4px;
  --spacing-medium: 8px;
  --spacing-large: 16px;
}

🧩 使用例

🔹 CSS 側

.button {
  background: var(--color-primary);
  padding: var(--spacing-medium);
}

🔹 TypeScript 側


import { tokens } from "@/tokens";

export const Button = () => {
  return (
    button
      style={{
        backgroundColor: tokens.color.primary,
        padding: tokens.spacing.medium,
      }}
    >
      ボタン
    button>
  );
};

🚀 まとめ

TypeScript で型安全にデザイントークンを管理できる
⚡️ Vite プラグインでビルド時に自動生成できる
🎨 Figma のデザインとコードをズレなく一元管理できる



Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -