🌟 はじめに — 目的と背景
「デザイントークンをコードで一元管理したい」
これまでは私のプロジェクトでも、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 のデザインとコードをズレなく一元管理できる
Views: 0