動画で読む
結論
Tailwind のユーティリティクラスは、ほとんどが「class 名 → CSS プロパティと値」の 1
マッピングで、text-sm は font-size: 0.875rem、px-4 は padding-inline: 1rem と命名から CSS が逆引きできるように設計されています。Qiita で honda-dev-jp 氏が公開した Tailwind→CSS 変換ツールはこの対応関係をブラウザ上で可視化していて、命名規則の設計思想を読み直す良いきっかけになりました。
ただし、ユーティリティの命名は「プロパティと値の射影」までは綺麗に行く一方で、デザインシステムが扱う「役割」(paper / ink / cat-engineering といったセマンティック層)には届きません。本稿はその境目を、AetherEchoes が採用している paper / ink / cat-* というセマンティックトークン体系と、Tailwind v4 の @theme で utility と token を合流させた実装をもとに整理します。
Tailwind ユーティリティと CSS プロパティの 1 対応を読む
ユーティリティ名は CSS プロパティ名の略記と値の略記の合成で、honda-dev-jp 氏のツールに bg-cyan-500 を入れると background-color: oklch(71.5% 0.143 215.221) が返ります。代表的な変換ルールはこうです。
text-{size}→font-sizeと対応するline-heightのセットpx-{n}→padding-inline: {n × 0.25rem}shadow-{level}→ 事前定義されたbox-shadowの段階セットrounded-{level}→border-radiusのスケール (none / sm / md / lg / xl / 2xl / full)
ここで気をつけたいのは「m-4 の 4 は 1rem であって 4px ではない」という点です。Tailwind の数値スケールは 0.25rem 刻みを採用していて、m-1 = 0.25rem / m-4 = 1rem / m-16 = 4rem になります。私は Tailwind に触りたての頃、これを 4px = m-4 と勘違いして、レイアウトが想定の 4 倍に膨らみました。CSS の単位感覚がそのまま使えるわけではない、というのが最初の落とし穴です。
text-sm が CSS 1 行に落ちない理由
ユーティリティ名は ある程度の意図的な抽象化を持っていて、text-sm は単に font-size: 14px ではなく「sm スケールのテキスト」、つまり次のような 2 行のセットです。
.text-sm {
font-size: 0.875rem;
line-height: 1.25rem;
}
「1 クラスで関連プロパティをセットで吐く」のがユーティリティの本質で、ここが素の CSS との最大の差分です。text-sm のような複合 utility は「タイポグラフィの 1 サイズ」という単位での命名であって、CSS プロパティの直訳ではありません。
utility-first の命名思想 — なぜ class が CSS そのままじゃないか
utility-first の発想は「セマンティックな class 名(.hero-title / .btn-primary)を捨て、CSS プロパティを class で呼ぶ」というものです。<h1 class="hero-title"> ではなく <h1 class="text-4xl font-bold tracking-tight"> と書きます。
この発想の利点は、設計を 1 箇所(.hero-title という抽象)に閉じ込めなくて済むことです。.hero-title を 5 ページで使い回した結果、6 ページ目で「ここだけ少し違う見出しが欲しい」となって .hero-title--alt を生やす、というアンチパターンが消えます。代わりに、6 ページ目で違う組み合わせの utility を書けば済みます。
ただし、色の話になると utility-first は途端に苦しくなります。bg-blue-500 を 30 箇所に書いた後で「青を変えたい」となった瞬間、grep して 30 箇所書き換えるか、CSS 変数を経由するか、theme を変えるかになります。私は AetherEchoes の 5 部門色 (cat-engineering / cat-design / cat-ai / cat-process / cat-essay) を一度 bg-blue-500 系で書いてみて、半年後に色の微調整を入れた時に 100 箇所近く触る羽目になりました。utility-first は色だけは別レイヤーに逃がしたほうが良い、というのが私の結論です。
AetherEchoes が paper/ink/cat-* を選んだ理由
AetherEchoes は CSS 変数を 4 階層で組んでいます。詳細は B+A Hybrid デザインシステム採択の記録 に書きましたが、ざっとこの構造です。
- base color (
paper/ink/rule/accent) - category color (
cat-engineering/cat-design/cat-ai/cat-process/cat-essay) - role token (
hero/body/hairline/dropcap) - component class (
.reader-prose/.ad-slot/.legal-prose)
ここで bg-cyan-500 ではなく bg-cat-engineering というセマンティック utility を生やしているのは、「色そのもの」ではなく「色の役割」を class 名に載せるためです。Tailwind 直 utility はプロパティと値の射影なので、cyan-500 が「engineering 部門」を表しているという情報は class 名からは読めません。役割が読めない class 名は、半年後の自分が読み返したときに「なぜここが青なのか」を本文ではなく git log から思い出すことになります。
Tailwind v4 @theme で utility と token を合流させる
@theme の使い方は 3 ステップで終わります。
@themeブロックで--color-*/--font-*/--spacing-*を定義- その変数名が自動で utility になる(
--color-cat-engineering→bg-cat-engineering/text-cat-engineering/border-cat-engineering) - dark mode は
@themeの上書きで完結([data-theme="dark"] { --color-paper: ... })
@theme {
--color-paper: oklch(98% 0.005 80);
--color-ink: oklch(20% 0.02 80);
--color-cat-engineering: oklch(62% 0.18 230);
--color-cat-design: oklch(75% 0.18 90);
}
[data-theme="dark"] {
--color-paper: oklch(12% 0.005 80);
--color-ink: oklch(95% 0.01 80);
}
この組み合わせで、AetherEchoes の Reader / Admin 両画面が bg-paper 1 個で light/dark の両方に追従します。v3 時代のように dark:bg-zinc-900 を 80 箇所書く必要がなくなり、トークン反転だけでテーマ切り替えが終わります。
私は最初 v3 で bg-[--cat-engineering] と書いていて、v4 移行のときに @theme で全部を書き直したら 80 箇所くらいの任意値記法が消えました。任意値記法は出てくる場面が局所的なら良いのですが、デザインシステム全体に広がると「ここは utility でここは任意値」の判別コストが膨らみ、ある時点から「全部 utility に揃える」ほうが安くなります。私の場合、その閾値は約 50 箇所でした。
なお、v4 の @theme は build 時に展開されるため、docker compose restart frontend を打たないと新しいトークンが反映されないケースが時々あります。HMR が走らないときは restart で諦める、というのが今のところの私の対処です(過去の私はここで 30 分溶かしたので、未来の私への申し送りとして書いておきます)。
まとめ
Tailwind のユーティリティは CSS プロパティと値の射影として綺麗に設計されていて、text-sm / px-4 / shadow-md を CSS に逆翻訳するのは難しくありません。けれど「役割」(paper / ink / cat-engineering)はユーティリティの抽象化レイヤーには載っていないため、デザインシステムを乗せたい場合は @theme で utility と token を合流させる必要があります。
honda-dev-jp 氏のツールが教えてくれるのはユーティリティ→CSS の対応関係までで、その先の「ユーティリティ→デザインシステム」を埋めるのは、各プロジェクトが @theme と命名で組み立てるしかありません。私の場合、v4 移行で @theme を導入してから、レイヤー 1 (base) と 2 (category) を class 名で直接呼べるようになり、デザインシステムが「設計図」から「日常運用できる道具」に変わった、というのが半年やってみた実感です。
Tags
よくある質問
- Tailwind の `m-4` は CSS の何 px に対応しますか?
- `m-4` は `margin: 1rem` です。Tailwind の数値スケールは 0.25rem 刻みなので `m-1 = 0.25rem` / `m-4 = 1rem` / `m-16 = 4rem` になります。「4 = 4px」ではないので注意してください。
- `bg-cyan-500` ではなく `bg-cat-engineering` を使うメリットは何ですか?
- color そのものではなく role を class 名に載せられる点です。`cyan-500` だけだとどの部門のための色か読めず、半年後に色を変えたい時に grep するしかなくなります。`@theme` で `--color-cat-engineering` を定義しておけば、Tailwind v4 が自動で `bg-cat-engineering` を生やしてくれます。
- Tailwind v4 `@theme` で定義した変数は dark mode でどう切り替えますか?
- `[data-theme="dark"] { --color-paper: ... }` のように、`@theme` の外で同じ変数名を上書きする CSS ブロックを書きます。utility 側 (`bg-paper` など) は変更不要で、トークン値の反転だけで light/dark の切り替えが完結します。
- v3 の任意値記法 `bg-[--cat-engineering]` を `@theme` に書き換えるべき閾値はありますか?
- 私の場合は約 50 箇所が分岐点でした。それ以下なら任意値記法でも判別コストは許容範囲ですが、それ以上になると「ここは utility / ここは任意値」の選別が grep の手間として効いてくるので、`@theme` で utility 側に揃えるほうが速いです。