第22章 UI コンポーネントを作る
22.1 「重複」をどう捉えるか
まず大前提として、Tailwind における重複は「悪」と決めつけないことが大切です。p-4・bg-white のようなユーティリティが画面のあちこちに現れるのは、第3章で見たとおり設計どおりであって、問題ではありません。生成される CSS は .p-4 ひとつだけなので、いくら使っても CSS は増えません。
問題になるのは、「ボタン」「カード」のような意味のあるまとまりが、長いクラス列ごと何度も複製されるときです。
<!-- 同じボタンを 3 か所に……長いクラス列が複製される -->
<button class="inline-flex items-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700">保存</button>
<button class="inline-flex items-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700">送信</button>
<button class="inline-flex items-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700">更新</button>
これだと、ボタンのデザインを変えるとき 3 か所すべてを直す必要があり、修正漏れも起きます。これこそが「コンポーネント化で解決すべき重複」です。
重複への対処には、大きく 2 つの方向があります。1 つは @apply で CSS にまとめる方法、もう 1 つはテンプレート/コンポーネントとして抽出する方法です。順に見ていきます。
22.2 @apply の役割と落とし穴
@apply は、既存ユーティリティの宣言を自分の CSS ルールに展開するディレクティブです。ここで初めて使い方を説明します。
@layer components {
.btn-primary {
@apply inline-flex items-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700;
}
}
<button class="btn-primary">保存</button>
components レイヤーに置くと、必要な箇所では通常のユーティリティで上書きできます。小さなボタンの見た目だけを共有するときに使える方法です。一方、クラス名と CSS の管理が増え、HTML 構造やイベント処理の重複は残ります。クラス列を短くするためだけにすべての要素を独自クラスへ移すと、管理する場所が増えることに注意しましょう。
22.3 @apply よりコンポーネント抽出が推奨される理由
本書では、構造や振る舞いも繰り返す UI は、まずテンプレートやコンポーネントに抽出する方針を勧めます。見た目に加え、ラベル、アイコン、無効状態なども1 か所で管理できるからです。これは設計上の推奨で、@apply を禁止する仕様ではありません。
公式ドキュメントも、ループやコンポーネントによる再利用に加え、ERB/Twig で小さなボタンまで partial にする負担が大きい場合にはカスタム CSS を使う選択肢を示しています。公式の重複管理
単一要素の見た目だけを共有したいなら CSS クラス、複数要素の構造や処理も共有したいならテンプレートの部品、というように、何を再利用したいかで判断しましょう。自分で変更できない外部マークアップに CSS を当てる場合も、@apply を使えます。
22.4 ボタンを題材にした抽出
「抽出」とは具体的にどうすることか。同じボタンを、各環境での部品にまとめてみます。クラス列が現れるのは定義の 1 か所だけになり、使う側はすっきりします。
テンプレートエンジンを使う場合: データを配列にしてループで描画すれば、クラス列は1 か所で済みます。素の静的 HTML にはループ機能がないので、小さな重複を残すか、見た目をカスタム CSS にまとめるかを選びます。
Rails(部分テンプレート):
<%# app/views/shared/_button.html.erb %>
<button class="inline-flex items-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700">
<%= label %>
</button>
<%= render "shared/button", label: "保存" %>
React(コンポーネント):
function Button({ children }: { children: React.ReactNode }) {
return (
<button className="inline-flex items-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white shadow-sm hover:bg-blue-700">
{children}
</button>
)
}
<Button>保存</Button>
どちらも、長いクラス列は部品の中に 1 度だけ書かれ、呼び出し側は render "shared/button" や <Button> と書くだけです。これが「重複を CSS ではなくコンポーネントに畳み込む」ということです。
22.5 カスタムユーティリティとコンポーネントクラスの境界
それぞれの仕組みは、役割が異なります。
| 仕組み | 役割 | 主な用途 |
|---|---|---|
@apply | 既存ユーティリティの宣言を CSS ルールへ展開 | 独自クラスや外部マークアップのスタイル |
@utility | バリアントと組み合わせられるユーティリティを登録 | プロジェクト独自の拡張 |
| テンプレート/コンポーネント | HTML の構造・見た目・振る舞いを共有 | 繰り返す UI 部品 |
たとえば、次のように新しいユーティリティを定義できます。
@utility content-auto {
content-visibility: auto;
}
content-auto や hover:content-auto として使えます。ここでは単一宣言の例ですが、@utility は複数の宣言も扱えます。「宣言が1 つか複数か」だけで使い分ける仕組みではありません。バリアント対応が不要な見た目のまとまりは @layer components のクラスに置く方法もあります(第26章)。