実際に作業したリポジトリはこちらで公開しています。
概要
TailwindCSSは非常に強力なフレームワークですが、従来のバージョンではプロジェクトが大規模になるといくつかの課題が生じることがありました。
プロジェクトが大規模になると以下の問題が発生する
- 特定のスタイルを上書きするために詳細度の高いセレクタを使用する必要がある
dark:
プレフィックスを多用することでHTMLが肥大化する- 可読性が低下し、保守性が悪化する
TailwindCSS 4.xで導入された@layer
機能を中心とした構造的なアプローチで課題を解決
@layer
による層分離でスタイルの適用順序を予測可能にする- CSS変数によるテーマ管理で保守性を向上させる
data-theme
属性による自動的なテーマ切り替えを実現する
設計の要点
この設計の主なポイントは以下の通りです。
-
@layer
による層分離: スタイルをtheme
,base
,components
,utilities
の4層に分けます。これにより、CSSの適用順序が予測可能になり、詳細度の競合を防ぎます。 -
CSS変数によるテーマ管理: 配色やフォントなどの値をCSS変数として一元管理します。これにより、テーマの変更がCSSファイル一箇所への修正で完結し、保守性が向上します。
-
data-theme
属性によるテーマ切り替え: JavaScriptでdata-theme
属性を制御します。TailwindCSS 4.xはこれを自動で認識し、dark:
バリアントを適用するため、追加の設定は不要です。
実装手順
1. TailwindCSSの設定
まず、tailwind.config.js
で、ユーティリティクラスが直接的な色の値ではなくCSS変数を参照するように設定します。これが動的なテーマ変更の鍵となります。
/** @type {import('tailwindcss').Config} */
export default {
// ...
theme: {
extend: {
colors: {
// 'surface'という名前で色を定義し、値にはCSS変数を指定
surface: {
DEFAULT: 'var(--color-surface)',
secondary: 'var(--color-surface-secondary)',
},
'text-primary': 'var(--color-text-primary)',
},
}
},
plugins: [
function({ addComponents }) {
addComponents({
// ボタンの基本スタイルを.btn-baseとして定義
'.btn-base': {
'@apply inline-flex items-center justify-center ...': {},
// ...
},
// ボタンの配色を.btn-primary-colorsとして定義
'.btn-primary-colors': {
'@apply bg-accent-primary text-white ...': {},
// ...
},
})
}
]
}
2. レイヤーの定義
次に、global.css
でCSSの層構造を定義します。この@layer
の記述順(theme
→ base
→ components
→ utilities
)がそのままCSSの優先順位となり、意図しないスタイルの上書きを防ぎます。
@config "./tailwind.config.js";
@import "tailwindcss";
@layer theme, base, components, utilities;
@import "./layers/theme.css" layer(theme);
@import "./layers/base.css" layer(base);
@import "./layers/components.css" layer(components);
@import "./layers/utilities.css" layer(utilities);
前述の通り、この構成では@custom-variant
の追加設定は不要です。
3. 各レイヤーの作成
定義した層ごとに、役割に応じたCSSファイルを作成します。
theme.css
: デザインシステムの根幹となるCSS変数を定義します。:root
でライトモードの値を、[data-theme="dark"]
セレクタ内でダークモードの値を定義することで、テーマの切り替えを実現します。
:root {
color-scheme: light;
--color-surface: var(--color-white);
--color-text-primary: var(--color-gray-900);
/* ... */
}
[data-theme="dark"] {
color-scheme: dark;
--color-surface: var(--color-gray-900);
--color-text-primary: var(--color-white);
/* ... */
}
base.css
: body
の背景色やフォントなど、サイト全体に適用される基本的なスタイルを記述します。
components.css
: UIの役割に基づいたクラス(例: .btn-primary
)を定義します。内部で@apply
を使い、config
で定義したbtn-base
やbtn-primary-colors
などを組み合わせます。
/* Button */
.btn-primary {
@apply btn-base btn-primary-colors;
}
.btn-secondary {
@apply btn-base btn-secondary-colors;
}
4. テーマ切り替えスクリプト
OSのテーマ設定に自動で追従する、シンプルなスクリプトをpublic/scripts/
以下に配置します。このスクリプトは<html>
タグのdata-theme
属性を動的に書き換える役割を担います。
(function() {
function applyTheme(isDark) {
document.documentElement.setAttribute('data-theme', isDark ? 'dark' : 'light');
}
function getSystemTheme() {
return window.matchMedia('(prefers-color-scheme: dark)').matches;
}
applyTheme(getSystemTheme());
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', e => applyTheme(e.matches));
})();
5. コンポーネントへの適用
コンポーネントファイルでは、components.css
で定義した、役割を示すクラス名を使用します。
<div class="card-interactive overflow-hidden">
<div class="p-6">
<h3 class="heading-3 mb-2">
{title}
</h3>
<p class="body-text mb-4">
{description}
</p>
<button class="btn-primary">
{buttonText}
</button>
</div>
</div>
このように、HTML側ではbg-blue-500 text-white ...
のようなユーティリティクラスの羅列を避けることができます。コンポーネントの構造がすっきりと見やすくなり、テーマに応じた色の切り替えはCSS側で自動的に処理されます。
まとめ
@layer
によってスタイルの優先順位が固定されるため、意図しない上書きや!importantの使用を避けることができます。テーマ関連の指定をCSS変数に集約することで、デザインシステムの一貫性を保ちながら、変更にも柔軟に対応できます。HTMLにはコンポーネントの役割を示すクラスのみを記述するため、マークアップが簡潔になり、可読性も大きく向上します。加えて、TailwindCSSの機能により、最小限の設定でdata-theme属性とdark:バリアントが自動的に連携する点も、この設計の大きな利点です。