Tailwind CSS で Design tokens を継承する（Global tokens と Alias tokens を分ける）ための検証

## この記事の概要

システマチックなプロダクト開発をするにあたり、Design tokens の存在は当たり前になってきました。
その際、以下の図にあるように、いくつかの階層を作って管理するパターンが多いです。

> ![](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/214677/847dd691-ae92-e07d-021e-c35bef0eddcf.png)
> 引用: [Design tokens - Spectrum](https://spectrum.adobe.com/page/design-tokens/)

こういった考え方を [Tailwind CSS](https://tailwindcss.com/) で実現しようと思った際に少し苦労したので、備忘録として記事に残しておきます。

なおこの記事では分かりやすくするため colors だけに言及していますが、fontSize や spacing などでも同様です。

## 検証した環境

次のリポジトリで検証していました。

https://github.com/xrxoxcxox/qiita-tailwind-design-system

Tailwind CSS のバージョンは 3.3.3 です。
全体の構成は次の通りです。

```sh
.
├── dist
│   └── output.css
├── index.html
├── input.css
├── node_modules
├── package.json
├── pnpm-lock.yaml
└── tailwind.config.js
```

開発時は次のコマンドを叩いています。
[Tailwind CSS の Installation](https://tailwindcss.com/docs/installation) に記載してあるのとほぼ同じで、変わったことは何もしていません。

```sh
npx tailwindcss -i ./input.css -o ./dist/output.css --watch
```

## 実現したい状態

- Global tokens を指定する
    - デフォルトの colors
    - 上記に加えブランドカラーのパレット
- Alias tokens を指定する
    - `surface = gray.100`, `primary = brand.500` など
- 基本的には alias tokens を使うものの、 global tokens も使えるままにしておく

どこかで定義した色に対してエイリアスを貼れる方法はないものかと探しました。

## 結論: 一旦の完成系

次のように、`module.exports` の外で `brandColors` を定義することで、`bg-gray-100` のような指定も `bg-surface` のような指定も、どちらもできました。

```javascript:tailwind.config.js
/** @type {import('tailwindcss').Config} */

const brandColors = {
  50: "#e3f7df",
  100: "#bfeeb4",
  200: "#a5e987",
  // ...
  950: "#0a3900",
};

module.exports = {
  // ...
  theme: {
    extend: {
      colors: ({ theme }) => ({
        brand: {
          ...brandColors,
        },
        surface: theme.colors.gray[100],
        "on-surface": theme.colors.gray[950],
        primary: brandColors[500],
        "on-primary": brandColors[50],
        "primary-container": brandColors[100],
        "on-primary-container": brandColors[600],
      }),
    },
  },
  // ...
};
```

## 背景: 通常の Tailwind の指定の仕方

最初にドキュメントなどを眺めていたときは `実現したい状態` のためには、`tailwind.config.js` がこんな雰囲気になりそう？と感じていました。

```javascript:tailwind.config.js
module.exports = {
  // ...
  colors: {
    brand: {
      50: "#e3f7df",
      100: "#bfeeb4",
      // ...
      500: "#55c500",
      // ...
      950: "#0a3900",
    },
    primary: "#55c500", // brand[500] とまったく同じ hex 値なのでイマイチ
    "on-primary": "#e3f7df", // brand[50] とまったく同じ hex 値なのでイマイチ
    // ...
  },
  // ...
};
```

実際、Tailwind にはデフォルトでは良い感じにエイリアスを貼れる方法がありません[^alias]。

[^alias]: 私が見つけられていないだけ、とも疑っているので、もしあれば教えていただきたいです！

Alias tokens を使わない or 上記のようにハードコーディングしてもそこまで大きな問題は無いような気がしつつ、好奇心で色々調べてみた次第です。

## 試したけどダメだった内容

### theme 直下で指定する

```javascript:tailwind.config.js
module.exports = {
  // ...
  theme: {
    colors: {
      brand: {
        50: "#e3f7df",
        100: "#bfeeb4",
        200: "#a5e987",
        // ...
        950: "#0a3900",
      },
    },
    extend: {},
  },
  // ...
};
```

この指定だと、alias tokens がどうとか以前に Tailwind のデフォルトの色がすべてなくなってしまいます。

`extend` を使わずに `theme` に直接追加するのは `元々あるパレットに追加` ではなく `1 からすべてを自分で作りたいとき` なので、論外です。

公式ドキュメントのうち、次の部分にも記載がありました。

https://tailwindcss.com/docs/theme#overriding-the-default-theme

### extend 内で新たな色を指定して `theme()` を使う

```javascript:tailwind.config.js
module.exports = {
  // ...
  theme: {
    extend: ({ theme }) => ({
      brand: {
        50: "#e3f7df",
        100: "#bfeeb4",
        200: "#a5e987",
        // ...
        950: "#0a3900",
        primary: theme("colors.brand[500]"),
        "on-primary": theme("colors.brand[50]"),
      },
    }),
  },
  // ...
};
```

公式ドキュメントのうち、次の部分を読んで勘違いしていました。

> If you need to reference another value in your theme, you can do so by providing a closure instead of a static value. The closure will receive an object that includes a theme() function that you can use to look up other values in your theme using dot notation.

https://tailwindcss.com/docs/theme#referencing-other-values

ですが、よく読めば `another value in your theme` と書いてあります。
`colors` の中で `colors` を参照することはできませんでした。
その証拠に（？）コンソールに出るエラーも `RangeError: Maximum call stack size exceeded` でした。

## 上手くいったきっかけ

次のドキュメントを読みました。

https://tailwindcss.com/docs/presets#merging-logic-in-depth

この内容をそのまま解釈すると「自分でプリセットを用意できる」ですが、それはそれで既存の設定が失われてしまいます。

ただ、このように `module.exports` 外のオブジェクトを引っ張ってくるのもありなら、上手く粒度を分ければ解決するのでは？と思ったのです。

冒頭に記載した通り `module.exports` 外に `brandColors` のオブジェクトを用意して、`extend` 内で展開し、alias の指定も `brandColors` をもとにすることで解決できました。

## 最後に

Global tokens と Alias tokens （規模やチームによっては Component specific tokens も）の階層を分けてスタイリングをするのはもう当たり前かと思っていましたが、これだけ流行っている Tailwind CSS でデフォルトでサポートされていないのは意外でした。

認知負荷を下げる意味でも Alias tokens までは設定する方が良いと思っています。
似たような考えの人の助けになれば幸いです。

---

最後まで読んでくださってありがとうございます！
[X (Twitter)](https://twitter.com/xrxoxcxox)でも情報を発信しているので、良かったらフォローお願いします！

[Devトーク](https://jobs.qiita.com/employers/qiita-inc/dev_talks/489)でお話してくださる方も募集中です！