はじめに
Tailwind、みんな大好きですよね!
今やどのweb開発現場に行ってもTailwindが使われています。
ところで、Tailwindで以下のようなコードを見たことがありませんか?
<div class="flex items-center space-x-2">...</div>
...
<div class="mt-3 flex space-x-2 overflow-hidden">
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" ... />
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" ... />
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" ... />
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" ... />
</div>
divの内側で同じスタイルのimgが4回繰り返されていますね。みなさんはエンジニアですから、こういうコードを見ると
「共通化したい!」
と思うかもしれません。
そこで、スタイルを共通化する方法がないか Tailwindの公式ドキュメント を探してみます。
"Managing duplication" と書かれた章があります。これを覗いてみると次の文章が書いてあります。
- Using multi-cursor editing
(マルチカーソル機能を使え! (迫真))
なんと、最初にマルチカーソル機能を使えと書いてあります(!!!!)
要するに、重複箇所を全部マルチカーソルでいっぺんに編集しろということです。
最初からTailwindの機能じゃありません。エンジニアらしからぬ泥臭さです。
マルチカーソルとは?
この記事が分かりやすいので練習してみてください。
https://qiita.com/TomK/items/3b1f5be07d708d7bd6c5
この後のドキュメントを見ると、
- Reactなどのループ(繰り返し)を使え
- コンポーネントごとに切り分けろ
と続きます。結局Tailwind特有の共通化方法は紹介されないか、出てきても非推奨なものばかりでした。
これはなぜだか、説明できますか?
実は、 Tailwindではスタイルを積極的に共通化するのは推奨されていません。
本記事ではTailwindの歴史と背景を追いつつ、その哲学を知ることで、この理由を解き明かしていきます!
Tailwind CSS の特徴をおさらい
そもそも Tailwind CSS を使うメリットは何でしょう?
Tailwindの本質的なメリットは、次のように挙げられます。
- 見た目を素早く記述できる (単純にそう。使えば分かる)
- 変更が安全 (スタイルの影響範囲がその要素に限定されるため)
- コードが分かりやすい (書き方が統一されるため、古いコードでも理解しやすい)
- 移植性が高い (構造と見た目がひとまとまりになっているため)
(公式ドキュメントより引用)
カッコ内の特徴は、これまでの HTML + CSS で記述していた class によるスタイリングとの本質的な違いです。
(これは何気にTailwindのもたらす価値を理解するうえでとっても大切な4つの項目だと思います。言語化できるの大事)
Tailwindの哲学「Utility-First」
Tailwind 公式ドキュメントには、 Utility First という言葉が登場します。
これはシンプルで、
「w-8
, flex
, font-bold
のような、一つのスタイルだけを記述する小さな ユーティリティクラス を組み合わせてUIを構築していこう!」
という考え方です。これはTailwindのメインコンセプトになっています。
当時はこれがとても斬新で、旧来のclassを使わなくてもスタイルを記述できるというのがとても革新的でした。やはり何よりも、 classという "スタイルに構造を導入する仕組み" を破壊した ことが超BIGなブレイクスルーだったと言えます。
Tailwind の広まった歴史と背景
ここから、Tailwindの歴史と背景を少し説明して、なぜ今Tailwindが流行ってるのかを解説します。
伝統的な関心の分離: 構造はHTML、見た目はCSS
かつて、Web開発のベストプラクティスは「後続と見た目による関心の分離」でした。 これは、構造を担うHTMLと、見た目を担うCSSを、それぞれ別のファイルに記述するという考え方です。
.card {
background-color: white;
padding: 1rem;
border-radius: 0.5rem;
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1);
}
<div class="card">
やっふー
</div>
この方法では、CSSクラスにスタイルがまとまっているため、再利用性が高いと考えられていました。
ReactとDDDの登場!
ここで、Reactのようなコンポーネントベースのフレームワークが次々登場しました。同時に ドメイン駆動設計(DDD) の考え方が世間に広まっていきます。
ドメイン駆動設計(DDD)とは?
ビジネス領域(ドメイン)の世界とソフトウェアの設計を密接に対応させ、ビジネスの変化に柔軟に対応できるソフトウェアを開発する手法です。DDDの重要なプラクティスの一つに 「同じ関心を持つものを集める」 という考え方があります。
例えば、Reactプロジェクトでは、ファイルの種類(components, hooksなど)で分けるのではなく、「関心事」や「ビジネス領域(ドメイン)」でディレクトリを分割することがあります。次のようなfeaturesディレクトリを皆さん見たことがあるかと思います🌊🌊
▼ ドメインで分割されたディレクトリ構成の例
src/
├── features/
│ ├── catalog/ # 商品カタログ
│ │ ├── components/
│ │ │ └── Catalog.tsx
│ │ └── hooks/
│ ├── order/ # 注文管理
│ │ ├── components/
│ │ │ └── OrderPage.tsx
│ │ └── hooks/
│ └── account/ # 顧客アカウント
│ ├── components/
│ │ └── UserList.tsx
│ └── hooks/
└── lib/ # 共有ライブラリ
この構成の場合、商品カタログ、注文管理、顧客アカウントなどのドメインの各コンテクストに関して、それに関連するリソースを種類に関係なくすべて詰め込んでいることが分かります。(DDDのプラクティスです)
この考え方からすると、 「あるUIコンポーネントの構造(HTML)と見た目(CSS)は、互いに強い同じ関心 (同じコンポーネントを記述するコード) を持っている と言えます。
従来のCSSでは、この強い関心を持つ2つの要素が別々のファイルに分離され、 凝集度が低い状態 になっていました。これはDDDのプラクティスに反します。
Tailwindが起こしたブレイクスルー:凝集度の向上
ここでTailwind CSSが登場します。Tailwindは、 見た目をコンポーネント (構造) に直接書き込む というアプローチを取ります。
<div class="bg-white p-4 rounded-lg shadow-md">
</div>
これにより、 同じ関心を持つ構造と見た目が一つのファイルにまとまり、凝集度が劇的に向上 します。したがって React のようなコンポーネント指向のフレームワークが広まるにつれて、 Tailwind は広まっていきました。
TailwindとDRY原則
ここまで、Tailwindの歴史を振り返ってそのブレイクスルーを見ることでTailwindの本質的な価値をみてきました。
ここからは、冒頭に戻って、Tailwindで繰り返しが許容される理由を話していきます。
Utility-FirstとDRY原則の矛盾⁉️
Tailwindの哲学「Utility-First」
Tailwindの核となる哲学は 「Utility-First」 でした。これは、w-8
, flex
, font-bold
のような、一つの責務だけを持つ小さなユーティリティクラスを組み合わせてUIを構築していく考え方ですね。
しかし、このアプローチを取ると、最初に紹介したようなコードが生まれがちです。
<div class="flex items-center space-x-2">...</div>
...
<div class="mt-3 flex space-x-2 overflow-hidden">
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" ... />
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" ... />
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" ... />
<img class="inline-block h-12 w-12 rounded-full ring-2 ring-white" ... />
</div>
inline-block h-12 w-12 rounded-full ring-2 ring-white
のような長いクラスリストが何度も繰り返されており、DRY原則に反しているように見えます!
DRY原則とは?
DRY原則とは、「Don't Repeat Yourself」という言葉の頭文字で、要するに同じコードを繰り返すな!という意味です。ほかの種々の原則に比べるとプログラミングする人には分かりやすいかなと思います。
なぜTailwindは「繰り返し」を許容するのか?
多くの開発者は、この繰り返しを見て「共通化したい」と感じるでしょう。Tailwindにも、スタイルを共通化するための@apply
という機能が存在します。
@layer components {
.btn-primary {
@apply py-2 px-5 bg-violet-500 text-white font-semibold rounded-full shadow-md hover:bg-violet-700 focus:outline-none focus:ring focus:ring-violet-400 focus:ring-opacity-75;
}
}
しかし、公式ドキュメントでは @apply
の使用を推奨していません。なぜなら、 @apply
を使いすぎれば、結局元の HTML + CSS のclassによるスタイリングに戻って行ってしまうからです。
冒頭でも紹介した通り、実はTailwindでは ある程度の重複は許容される と考えられています。
これはなぜなのか、理由があります。
真の悪は、HTML内でのスタイルの繰り返しではなく、安易な共通化によって、本質的に異なるコンテクストのUIが誤って結合されてしまうこと なのです。
ショートストーリー: タロウくんの場合
この「安易な共通化の罠」の例として、ある新人開発者タロウ君の話を書いてみました。
-
ある日…
タロウ君は、プロジェクト内に見た目が全く同じ「商品購入ボタン」と「友達招待ボタン」を見つけました。
「これは共通化すべきだ!」と考えた彼は、CommonButton
コンポーネントを作成しました。function CommonButton({ type, text, onClick }) { return ( <button className="blue-rounded-button" onClick={onClick}> {text} </button> ); }
-
最初の変更要求
1ヶ月後、「購入ボタンだけローディング状態を表示したい」という要求が来ました。 彼はCommonButton
に条件分岐を追加します。 -
続く変更要求
さらに1ヶ月後、「招待ボタンだけアイコンを付けたい」という要求。またも条件分岐を追加。
そのまた1ヶ月後、「購入ボタンは在庫切れ時にグレーアウトしたい」という要求。 さらに条件分岐が追加されます。 -
そして伝説へ…
数ヶ月後、CommonButton
コンポーネントは無数の引数と複雑なif文の塊となり、誰もが触りたがらない保守不能なコードになってしまいました。function CommonButton({ type, text, onClick, isLoading, icon, isOutOfStock, invitedCount, // ... 引数が爆増 }) { const getButtonText = () => { if (type === 'purchase') { if (isOutOfStock) return '在庫切れ'; return isLoading ? '処理中...' : text; } // 大量のif文... }; // ... }
~YOU DIED~
何が問題だったのか?
タロウ君の間違いは、最初のステップにありました。
「商品購入」と「友達招待」という、全く異なるコンテクスト(ドメイン境界)に属するUIを、見た目が同じというだけで性急に共通化してしまったこと、これが全ての元凶です。
正しい共通化、DRY原則の適用
大切なこととして、DRY原則は、盲目的に適用するものではありません。DRY原則は、 消極的に、心の片隅に置いておく程度で構わない と考えています。
どんなときならDRY原則を適用 (=共通化) して良いのでしょう?
DRY原則を適用するときは、
- その繰り返しを修正するのが本当に面倒だと感じているか?
- 共通化しようとしている対象は、本当に同じコンテクストに存在し、未来永劫、本質的に同じ役割を持つと言えるか?
を念入りにチェックして、指差し確認して深呼吸してから (大事)、共通化するようにしましょう。
Tailwind の推奨する共通化
TailwindがHTML内でのクラスの繰り返しを許容する背景には、ReactやVueのようなコンポーネントベースのUIシステムの存在があります。
スタイル単位(例: .btn
クラス)で共通化するのではなく、コンポーネント単位で共通化すれば良いのです。
// PurchaseButton.tsx
export function PurchaseButton({ isLoading, isOutOfStock }) {
// ... 購入ボタンのロジックとスタイル
return (
<button className="py-2 px-5 bg-violet-500 ...">
{/* ... */}
</button>
);
}
// InviteButton.tsx
export function InviteButton({ icon }) {
// ... 招待ボタンのロジックとスタイル
return (
<button className="py-2 px-5 bg-violet-500 ...">
{/* ... */}
</button>
);
}
上記の例では、各ボタンコンポーネント内で同じTailwindのユーティリティクラスが使われています。しかし、それぞれのコンポーネントは異なる責務(コンテクスト)を持っているため、これで良いのです。これを共通化してしまうとタロウ君です。
そして、もしコンポーネントとして共通化できないのであれば、それはそもそもコンテクストが違うため、スタイルを共通化すべきではありません。結局、コンポーネントでスタイルを分けるのが必要十分で最適ということですね!
おわりに
Tailwind CSSは、ただの変わったスタイリング手法ではありません。
- 関心の分離を再定義し、構造と見た目の凝集度を高めることで、DDDのプラクティスと調和する。
- DRY原則に対して、「コンテクスト」という視点を導入する。
一見すると冗長に見えるクラスの繰り返しは、実はコンポーネントの独立性を保ち、長期的な保守性を高めるための意図的な設計思想なのです!
(この文章はLT会で登壇したスライドを記事にしたものです。https://speakerdeck.com/kotek/tailwind-nozhe-xue)
参考文献
- Tailwind CSS v3 公式ドキュメント: https://v3.tailwindcss.com/docs/installation
- Tailwind CSS v4 公式ドキュメント: https://tailwindcss.com/docs/installation