🔶

Terrazzoを使ってFigma VariablesをTailwindテーマに同期したい

に公開

これはヤプリ&フラー 合同アドベントカレンダー Advent Calendar 2025の13日目の記事です。

背景

FigmaにはVariablesというデザイントークンを定義する機能があります。色・サイズ・タイポグラフィ等のプロパティでVariablesを呼び出すことで、デザインファイルの統率を図ることができます。
実装者視点では、Figma VariablesをCSS変数やTailwindテーマにも展開できるとデザインファイルとの対応づけがしやすくなります。ただ、膨大な量の変数はコピペしていられないので自動化できると便利です。

DTCG(Design Tokens Community Group)

DTCG(Design Tokens Community Group)はデザイントークンの仕様策定を行うW3Cのコミュニティグループで、今年の10月に仕様のStableを公開しています。
DTCGが定めるJSONフォーマットでデザイントークンを管理することで、DTCGフォーマットに対応しているデザインツール同士のトークン共有や、コードベースへの適用が容易になります。

https://www.designtokens.org/

Figma Variablesをコードに適用する方法として、FigmaのAPI群(RESTやプラグイン等)からVariables JSONを取得し、それを任意のプラットフォームに変換することもできます。しかし、そもそも自分の手で変換器を実装する必要があったり、Figma API側の仕様変化に追従する必要があったりします。DTCGが誕生したことで、デザイン〜実装のトークン共有のためのエコシステムが強くなることが期待できます。

Terrazzo

TerrazzoはDTCGを各プラットフォームのコードに変換できるCLIツールで、12/11現在

  • Web(CSS, Tailwind, TS/JS等)
  • iOS(Swift)

の形式に対応しています。もともとCobaltというツール名で、他のツールと混同しやすいことと、よりツールの方向性を示せる命名のために改名したらしいです。
競合のツールとしてStyle Dictionaryがあり、そちらの方がAndroidなど出力形式は多いのですが、Tailwind v4のCSS First Configurationsに対応しているのがTerrazzoのみだったためこちらを使っています。

変換を試してみる

上記で紹介したTerrazzoを使って、実際にFigma Variablesをコードベースに適用できるようにしてみます。
大まかなフローは以下の通りです。

  1. Figma Variablesを公式機能orプラグインでDTCG JSONにエクスポートする
  2. エクスポートしたDTCG JSONをTerrazzoでTailwind themeに変換する

1. Figma Variables ∠( ・_・ )/ DTCG JSON

Figma VariablesをDTCGに変換するには、Figmaのexport modes機能を使う方法プラグインを使う方法の2つがあります。
export modes機能はつい先月リリースされたばかりの公式機能です。公式提供があればプラグインは不要なのでは?と思うかもですが、プラグインの方も機能が充実しているので紹介させてください。

また今回は、検証用のFigmaとしてデジタル庁デザインシステム デザインデータを手元にコピーして使用しています。

a. Figma export modesで出力する

https://help.figma.com/hc/en-us/articles/15343816063383-Modes-for-variables

Figma Variablesの1機能として提供されている方法で、各CollectionごとにDTCG JSONをエクスポートできます。
Variablesテーブルを開き、エクスポートしたいCollectionを右クリックすると選択できます。
figma variablesテーブルでcollectionを選択し、ドロップダウンメニューからexport modesをクリックしようとしている

機能名にmodesと入っている通り、VariablesのModes機能を使っている場合は各ModeごとにJSONが分割されてダウンロードされます。
また、1回のExport modesで1つのCollectionのみをエクスポートできるため、複数のCollectionがあるファイルではCollectionの数分エクスポートを行う必要があり少し手間がかかります。

b. プラグイン(TokensBrücke)で出力

Terrazzoの公式ドキュメントでも紹介されている、FigmaプラグインのTokensBrückeを使ってもDTCG JSONへのエクスポートが可能です。
TokensBrückeではファイル内の全てのCollectionをまとめて1ファイルにエクスポートするため、JSONでは各Collectionにネストした形でトークンが格納されます。
また、Stylesのエクスポートにも対応していたり、GitHubなど任意のサーバにpushできたりと痒いところに手が届く機能が備わっています。

TokensBrückeのUIで、エクスポート設定とエクスポート結果が表示されている

今回TerrazzoにJSONを読み込ませるために、以下の設定を行いました:

  • Typography-styles
    • オフ。Typographyから貼っているfontFamilyのエイリアスの型が合わなかったため変換に失敗してしまう☠️
    • あくまでfont関連のプロパティのセットなので、コンポーネントとして実装した方がいいかも。
  • Grid-styles
    • オフ。Tailwind themeの名前空間に相当するものがない。
  • Color-styles
    • オフ。デザインファイルで定義されていない
  • Effect-styles
    • オン。shadowが変換できるため。
  • Include .value string for aliases
    • オフ。Terrazzoがaliasとして認識してくれなかった。
  • Use DTCG keys format
    • オン。DTCGのフォーマット上、各トークンのプロパティ(type, value等)には$接頭辞が必要なため?

ここはTerrazzoで実際に変換を試しながら設定を調整する必要がありました。

エクスポートされたDTCG JSONを見てみると、Figmaのcollection/groupのネスト構造がそのまま反映されていることがわかります。

digital-go.tokens.json
{
  "Color": { 
    "Color": {
      "Primitive": {
        "Blue": { 
          "50": { // "Color" collection の "Color/Primitive/Blue/50" variable に対応
            "$type": "color",
            "$value": "#e8f1fe",
            "$description": "",
            "$extensions": {
              "mode": {}
            }
          },
          "100": {
            "$type": "color",
            "$value": "#d9e6ff",
            "$description": "",
            "$extensions": {
              "mode": {}
            }
          },
          "200": {
            "$type": "color",
            "$value": "#c5d7fb",
            "$description": "",
            "$extensions": {
              "mode": {}
            }
          },
          ...
        }
      }
    }
  }
}

2. DTCG JSON ∠( ・_・ )/ Tailwind theme

2.1 Terrazzoをプロジェクトに導入する

DTCG JSONを手に入れたら、Terrazzoを導入してTailwind themeを展開していきます。
サンプルアプリとしてSvelteKitプロジェクトを作成しましたが、フレームワークは何でもいいです。

プロジェクト作成後、@terrazzo/cliをインストールしてconfig作成とプラグイン追加を行います。

pnpm i -D @terrazzo/cli
pnpm exec tz init  

TerrazzoはテンプレートとしてHIGやPrimerといった既存のデザインシステムを用意してくれています。
今回はデザインシステムを持っている前提のため、Noneを選択します。

tz initを実行し、デザイントークンのテンプレートを選択する

変換できるプラットフォームごとにプラグインが提供されているため、欲しい分だけ選択します。
注意点として、TailwindプラグインはCSSプラグインと共に入れる必要があります。また、色やサイズ等をスクリプトで扱いたい場合はTSプラグインもあると便利です。

追加したい変換プラグインを選択する

2.2 Tailwindの変換設定を行う

初期化作業が終わるとterrazzo.config.mjsが作成されるため、そこに変換設定を書いていきます。
設定項目として特に必要だったのは、トークンファイル、変換先のファイル、及びTailwindのマッピング設定です。

terrazzo.config.mjs
import { defineConfig } from '@terrazzo/cli';
import tailwind from '@terrazzo/plugin-tailwind';
import css from '@terrazzo/plugin-css';
import js from "@terrazzo/plugin-js";

export default defineConfig({
  tokens: ['./tokens/digital-go.tokens.json'],
  plugins: [
    js({
      js: "tokens.js",
      ts: "tokens.d.ts"
    }),
    css({
      filename: 'css-tokens.css'
    }),
    tailwind({
      filename: 'tailwind-tokens.css',
      theme: {
        /** @see https://tailwindcss.com/docs/configuration#theme */
        color: ['Color.Color.*'],
        text: ['Typography.FontSize.*'],
        "font-weight": ['Typography.FontWeight.*'],
        radius: ['Size.BorderRadius.*'],
        shadow: ['Effect-styles.Elevation.*']
      }
    })
  ],
  ...
});

トークンファイルの設定

トークンファイルは複数受け取ることができ、それらのトークンが全て変換先のファイルに展開されます。
Figma export modesのように、collection/modeごとにjsonが分割される状況下でも同じ変換ファイルにまとめられます。

terrazzo.config.mjs
tokens: ['./tokens/digital-go.tokens.light.json','./tokens/digital-go.tokens.dark.json'],

変換先のファイル

変換先のファイルは各プラグインのfilenameプロパティに指定します。プラグインごとに変換できるファイルは単一です。

手動でTailwind themeを編集したい点を考慮し、変換ファイルは自動生成物として分離しておき、プロジェクト内のglobals.cssでimportする形が良いと思います。

Tailwindへのマッピング

DTCG JSONのトークンがどこに・どこまでネストされるかは自由自在なため、Tailwind themeのcolor-text-といった名前空間との対応づけが必要になります。
今回の場合はJSON内のColor.Colorプロパティ内に全てのカラーが入っているため、globパターンでcolor: ['Color.Color.*']と指定することでそれ以下の変数がcolor-名前空間に変換されます。

ワイルドカード以降のネスト構造は-繋ぎで命名に展開されるため、長くならないように調整すると良いです。(例: Color.Color.Primitive.Blue.50--color-Primitive-Blue-50)

terrazzo.config.mjs
tailwind({
    filename: 'tailwind-tokens.css',
    theme: {
        /** @see https://tailwindcss.com/docs/configuration#theme */
        color: ['Color.Color.*'],
        text: ['Typography.FontSize.*'],
        "font-weight": ['Typography.FontWeight.*'],
        radius: ['Size.BorderRadius.*'],
        shadow: ['Effect-styles.Elevation.*']
    }
})

2.3 変換してみる

設定も一通り終わったら、@terrazzo/cliでビルドを実行します。
まず、ビルドコマンドをnpm scriptsに追加します。

packages.json
"scripts": {
    "tz:build": "tz build",
    ...
}

そしてnpm scriptsを実行すると、各トークンへの変換が実行されます。

pnpm run tz:build

tz:buildを実行し、変換されたトークン数が表示される

変換先のファイルを見ると、トークンがCSSやJSに変換されていることが確認できます↓

tailwind-tokens.css
@import "tailwindcss";

@theme {
  --color-Primitive-Blue-50: color(srgb 0.9098039215686274 0.9450980392156862 0.996078431372549);
  --color-Primitive-Blue-100: color(srgb 0.8509803921568627 0.9019607843137255 1);
  --color-Primitive-Blue-200: color(srgb 0.7725490196078432 0.8431372549019608 0.984313725490196);
  --color-Primitive-Blue-300: color(srgb 0.615686274509804 0.7176470588235294 0.9764705882352941);
  --color-Primitive-Blue-400: color(srgb 0.4392156862745098 0.5882352941176471 0.9725490196078431);
  ...
  --radius-24: 24px;
  --radius-32: 32px;
  --radius-Full: 9999px;
  --shadow-1: 0 1px 5px 0 color(srgb 0 0 0 / 0.30196078431372547), 0 2px 8px 1px color(srgb 0 0 0 / 0.10196078431372549);
  --shadow-2: 0 1px 6px 0 color(srgb 0 0 0 / 0.30196078431372547), 0 2px 12px 2px color(srgb 0 0 0 / 0.10196078431372549);
  --shadow-3: 0 1px 6px 0 color(srgb 0 0 0 / 0.30196078431372547), 0 4px 16px 3px color(srgb 0 0 0 / 0.10196078431372549);
  --shadow-4: 0 2px 6px 0 color(srgb 0 0 0 / 0.30196078431372547), 0 6px 20px 4px color(srgb 0 0 0 / 0.10196078431372549);
  --shadow-5: 0 2px 10px 0 color(srgb 0 0 0 / 0.30196078431372547), 0 8px 24px 5px color(srgb 0 0 0 / 0.10196078431372549);
  --shadow-6: 0 3px 12px 0 color(srgb 0 0 0 / 0.30196078431372547), 0 10px 30px 6px color(srgb 0 0 0 / 0.10196078431372549);
  --shadow-7: 0 3px 14px 0 color(srgb 0 0 0 / 0.30196078431372547), 0 12px 36px 7px color(srgb 0 0 0 / 0.10196078431372549);
  --shadow-8: 0 3px 16px 0 color(srgb 0 0 0 / 0.30196078431372547), 0 14px 40px 7px color(srgb 0 0 0 / 0.10196078431372549);
}

tokens.js
export const tokens = {
  "Color.Color.Primitive.Blue.50": {
    ".": {
      "colorSpace": "srgb",
      "components": [
        0.9098039215686274,
        0.9450980392156862,
        0.996078431372549
      ],
      "alpha": 1,
      "hex": "#e8f1fe"
    },
  },
  "Color.Color.Primitive.Blue.100": {
    ".": {
      "colorSpace": "srgb",
      "components": [
        0.8509803921568627,
        0.9019607843137255,
        1
      ],
      "alpha": 1,
      "hex": "#d9e6ff"
    },
  },
  ...
}

変換したトークンをアプリケーションで使ってみる

アプリケーションのコンポーネント実装で、変換したトークンを呼び出してみます。
以下のコードではTailwind theme形式とTS形式の両方を試しており、タグ要素の背景色をラベルと共に管理できるようにしています。

<script lang="ts">
  import { token } from './tokens';

  const tags = {
    succeed: {
      color: token('Color.Color.Primitive.Blue.900').hex,
      text: '成功'
    },
    fail: {
      color: token('Color.Color.Primitive.Red.900').hex,
      text: '失敗'
    }
  } as const;
</script>

<div class="p-4">
  <h1 class="text-2xl font-bold mb-4">welcome to the DTCG example app</h1>
  <button class="bg-Primitive-Blue-900 rounded-8 text-Neutral-White text-16 p-4 font-700 shadow-2"
    >ラベル</button
  >
  <button
    disabled
    class="bg-Neutral-SolidGray-300 rounded-8 text-Neutral-SolidGray-50 text-16 p-4 font-700"
    >ラベル</button
  >
  <div class="mt-4">
    {#each Object.entries(tags) as [key, tag]}
      <span
        class="inline-block px-3 py-1 rounded-6 text-Neutral-White text-16 font-700 mr-2"
        style="background-color: {tag.color}">{tag.text}</span
      >
    {/each}
  </div>
</div>

開発サーバにアクセスすると、トークンを適用したUIが確認できます\ ( 'ω')/ウオアアア

今回はデジタル庁のデザインシステムFigmaを参照しているため、色・影・角丸などがそれに対応したデザインで表示されています。

影付きの青いボタン、disabled状態のグレーボタン、青い成功タグ、赤い失敗タグが開発サーバのページに表示されている

感じた課題

1. Tailwind themeにVariablesの命名がそのまま乗っかってしまう

今回使用させていただいたデジタル庁のFigmaファイルでは、Variablesの命名がPascalCaseで統一されています。これをTailwindプラグインで変換した際、Tailwindの変数もPascalCaseで展開されました。

--color-Primitive-LightBlue-50: color(srgb 0.9411764705882353 0.9764705882352941 1);

CSS Variablesの命名は基本的にkebab-caseで行うため、実装者視点ではFigma Variablesもkebab-caseで統一されてほしいところではあります。

また、PascalCaseの場合はCSS Variablesとしては問題なく使えますが、スペース等のCSS Variablesの定義に使えない文字が入るとトークンが完全に使えなくなってしまいます。

// 変数名が途中で切れている(T_T)
--color-Primitive-Light Blue-50: color(srgb 0.9411764705882353 0.9764705882352941 1);

ただ、これらの問題はPlainな方のCSSプラグインでは起こらず、kebab-caseで統一される仕様になっているため、今後Tailwindプラグインでもサポートが入るかもしれません。

css-tokens.css
// "Success""Succ Ess"とぶった切ってみた場合
--color-color-semantic-succ-ess-1: var(--color-color-primitive-green-600);

2. Figmaでrem単位が扱えない

これはFigma自体のトークンの扱いの問題なのですが、余白・ブロックやフォントのサイズ単位はremではなくpxになってしまいます。特にフォントサイズは、ブラウザでのテキスト拡大設定を考慮してremで定義したいため、Figmaで定義したテキストサイズトークンは実装では扱うのが難しいと感じています。

Terrazzoのドキュメントでもトークン定義にはremを推奨している一方、Figmaのエクスポート時点でpxになってしまうのが問題のため、このあたり良い解決策を思いつく方は教えていただけると幸いです m(_ _)m

まとめ

今回はFigma VariablesからDTCGトークンを出力する方法と、それをTailwind themeに展開する方法について紹介しました。ある程度設定を行えば、Terrazzoを使って即座に大量のデザイントークンをコードベースに適用できるため、

  • プロジェクト初期のCSS整備を省略できる
  • Figma Variablesの変更を漏れなく適用できる

といった点でデザインと実装を対応させるための強力なツールになると感じました。
近年Figma MCPを使ったUI実装も注目されており、エージェントのスタイル実装に一貫性を持たせるために一役買うかもしれません(まだ試してないので予測ですが...)。

今回の参考実装はGitHubに乗せています。

https://github.com/9rotama/dtcg-example

ここまで読んでいただきありがとうございました!

Discussion