Design

Tailwind ユーティリティクラスは実 CSS のどこに落ちるか — 命名から読む utility-first

Tailwind のユーティリティクラスは text-sm や px-4 のように CSS プロパティと値の 1:1 マッピングですが、デザインシステムの「役割」までは届きません。AetherEchoes の paper/ink/cat-* 4 階層と、Tailwind v4 の @theme で utility と token を合流させた実装の観点で整理します。

2026年5月26日 09:09·12 min·SoraEndo
SoSoraEndo2026年5月26日 09:0912 min1,817

動画で読む

結論

Tailwind のユーティリティクラスは、ほとんどが「class 名 → CSS プロパティと値」の 1

マッピングで、text-smfont-size: 0.875rempx-4padding-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 デザインシステム採択の記録 に書きましたが、ざっとこの構造です。

  1. base color (paper / ink / rule / accent)
  2. category color (cat-engineering / cat-design / cat-ai / cat-process / cat-essay)
  3. role token (hero / body / hairline / dropcap)
  4. 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 ステップで終わります。

  1. @theme ブロックで --color-* / --font-* / --spacing-* を定義
  2. その変数名が自動で utility になる(--color-cat-engineeringbg-cat-engineering / text-cat-engineering / border-cat-engineering
  3. 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 側に揃えるほうが速いです。

参考文献

  1. Tailwind CSS Class to CSS (honda-dev-jp / Qiita)
  2. Tailwind CSS v4 — Theme variables (公式ドキュメント)
  3. MDN — CSS Custom Properties (--*)
  4. B+A Hybrid デザインシステム採択の記録 (AetherEchoes)

Reaction

Share

X (Twitter)