Tailwind CSS v4を本番投入して3ヶ月、設計思想が根本的に変わった話
「設定ファイルが変わる程度でしょ」と思ってたら甘かった。v4導入で最初の2週間チームが苦労した話と、3ヶ月後に「早く入れればよかった」と感じた理由を実コードで振り返ります。
Tailwind CSS v4を本番投入して3ヶ月、設計思想が根本的に変わった話
先日、うちのチームでTailwind CSS v4を本番プロジェクトに入れてちょうど3ヶ月が経った。正直なところ、v3からのアップグレードを舐めてたというか、「設定ファイルが変わる程度でしょ」くらいに思ってたんですよね。それが甘かった。ビルド速度や設定の思想がかなり根本的に変わっていて、最初の2週間はチーム全体がわりとしんどかった。でも今は戻る気は全くなくて、むしろもっと早く入れておけばよかったと思っているくらいです。
v4の何がどう変わったのかを実際のコードレベルで振り返りつつ、導入してわかった本音の部分を話せたらと思う。教科書的なドキュメントは公式に任せて、実務でぶつかった話を中心に。
v4で何が変わったのか——tailwind.config.jsが消えた衝撃
v4の一番大きな変化は、tailwind.config.jsが廃止されてCSSファイル自体が設定の起点になったことだと思う。最初は「は?」ってなった。3年間tailwind.config.jsでテーマをカスタマイズしてきたチームとしては、慣れ親しんだ場所が消えるのはかなり心理的抵抗があった。
v3ではこんな構成が当たり前だったわけで。
// tailwind.config.js (v3時代)
module.exports = {
theme: {
extend: {
colors: {
primary: '#3B82F6',
secondary: '#6366F1',
},
fontFamily: {
sans: ['Inter', 'sans-serif'],
},
},
},
plugins: [],
};
v4ではこれがCSSに移動する。
/* app.css (v4) */
@import "tailwindcss";
@theme {
--color-primary: #3B82F6;
--color-secondary: #6366F1;
--font-family-sans: 'Inter', sans-serif;
/* カスタムブレークポイントも */
--breakpoint-3xl: 1920px;
}
慣れると、CSSとJSを行き来しなくていい分むしろ見通しがよくなる。ただ、移行時に一番困ったのはプラグインの扱いで、v3で使っていた@tailwindcss/typographyや@tailwindcss/formsがv4対応版に更新されているかの確認が必要だった。2026年5月時点では主要なプラグインは軒並み対応済みになっているので、ここはもうほぼ問題ないと思う。
あと個人的に地味に刺さったのがCSS Layersのネイティブサポート。v4はブラウザのネイティブCascade Layersを使って、base・components・utilitiesを分離している。
/* v4が内部で生成するLayerの構造 */
@layer theme, base, components, utilities;
@layer base {
*, *::before, *::after {
box-sizing: border-box;
}
}
@layer utilities {
.flex { display: flex; }
.p-4 { padding: 1rem; }
}
このおかげで、既存のCSSとの優先度の衝突が劇的に減った。うちのプロジェクトは既存のCSS資産を抱えたまま段階的にTailwindを入れていたので、ここは本当に助かった。
Oxide Engineの速度、数字で見ると笑えるくらい違う
v4から導入されたOxide Engine(Rustベースのエンジン)のビルド速度は、正直最初は「そんなに変わらないでしょ」と懐疑的だったんだけど、実際に計測してみたら笑えないくらい違った。
うちのプロジェクト(Next.js 15ベース、約200コンポーネント)での計測結果がこちら。
| 計測項目 | v3 (PostCSS) | v4 (Oxide) | 改善率 |
|---|---|---|---|
| フルビルド(初回) | 3,200ms | 410ms | 約87%短縮 |
| インクリメンタルビルド | 280ms | 18ms | 約94%短縮 |
| HMR(Hot Module Replacement) | 120ms | 9ms | 約92%短縮 |
| CSSファイルサイズ(本番) | 48KB | 29KB | 約40%削減 |
xychart-beta
title "Tailwind CSS v3 vs v4 ビルド時間比較 (ms)"
x-axis ["フルビルド", "インクリメンタル", "HMR"]
y-axis "ビルド時間 (ms)" 0 --> 3500
bar [3200, 280, 120]
bar [410, 18, 9]
インクリメンタルビルドが280msから18msというのは、開発中のフィードバックループがほぼゼロ遅延に感じるレベルで変わった。特にスタイルを細かく調整しながらUIを作るフェーズで、この違いは体感として相当大きい。「あれ、反映された?」ってブラウザを二度見することがなくなった。
Oxide Engineが速い理由はいくつかあるんだけど、特に重要なのがコンテンツスキャンの最適化。v3はglobパターンで全ファイルをスキャンしてクラスを抽出していたけど、v4はRustで書かれたパーサーが差分更新を賢くやってくれる。設定で細かいチューニングをしなくても、デフォルトでかなり速い。
実際のコンポーネント設計がどう変わったか
v4を使い続けて気づいたのが、コンポーネントの書き方の習慣も少し変わってきたということ。
v3時代は@applyを多用するか、ユーティリティクラスをそのままJSXに並べるかの二択で、どっちも微妙さがあった。v4では@utilityディレクティブが追加されて、カスタムユーティリティをちゃんとTailwindの文脈で定義できるようになった。これが思ったよりずっと使い勝手がいい。
/* カスタムユーティリティをTailwindに統合 */
@utility card-base {
border-radius: var(--radius-lg);
padding: var(--spacing-6);
background: var(--color-white);
box-shadow: var(--shadow-md);
}
@utility btn-primary {
background-color: var(--color-primary);
color: var(--color-white);
padding: var(--spacing-2) var(--spacing-4);
border-radius: var(--radius-md);
font-weight: var(--font-weight-semibold);
transition: background-color 150ms ease;
&:hover {
background-color: color-mix(in srgb, var(--color-primary) 80%, black);
}
}
これをJSXで使うとこうなる。
// Card.tsx
export function Card({ children }: { children: React.ReactNode }) {
return (
<div className="card-base">
{children}
</div>
);
}
// Button.tsx
interface ButtonProps {
label: string;
onClick: () => void;
variant?: 'primary' | 'outline';
}
export function Button({ label, onClick, variant = 'primary' }: ButtonProps) {
return (
<button
onClick={onClick}
className={
variant === 'primary'
? 'btn-primary'
: 'border border-primary text-primary px-4 py-2 rounded-md font-semibold hover:bg-primary/10 transition-colors'
}
>
{label}
</button>
);
}
v3で@applyを使っていたころと比べて、カスタムユーティリティがTailwindのVariant(hover:, dark:, md:など)と組み合わせて動くのが本当に自然になった。地味に便利なのがhover:btn-primaryみたいな書き方をそのまま受け付けてくれること。@apply時代にこれができなくて地味にイライラしてたので、じわじわ嬉しい改善だった。
ただ、正直まだ設計のベストプラクティスは模索中で、「どこまでを@utilityにまとめてどこからをインラインで書くか」の線引きはチームによって好みが分かれると思う。うちは「3箇所以上で使うものは@utility化」というゆるい基準でやってる。
ダークモードとCSS変数の連携が別次元になった
v4で個人的に一番感動したのが、ダークモードの実装が劇的にシンプルになったこと。v3ではdark:プレフィックスを全クラスに付け回すか、JITで頑張るかという感じだったけど、v4ではCSS変数ベースのアプローチが推奨されていて、これがとても筋がいい。
@theme {
/* ライトモードのデフォルト */
--color-bg: #ffffff;
--color-text: #111827;
--color-surface: #f9fafb;
--color-border: #e5e7eb;
}
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #111827;
--color-text: #f9fafb;
--color-surface: #1f2937;
--color-border: #374151;
}
}
/* class-basedのトグルも対応 */
.dark {
--color-bg: #111827;
--color-text: #f9fafb;
--color-surface: #1f2937;
--color-border: #374151;
}
// コンポーネント側はシンプルに
function Layout({ children }: { children: React.ReactNode }) {
return (
<div className="min-h-screen bg-[--color-bg] text-[--color-text] transition-colors duration-200">
<main className="max-w-7xl mx-auto px-4 py-8">
{children}
</main>
</div>
);
}
CSS変数の値が自動的に切り替わるので、コンポーネント側はbg-[--color-bg]と書くだけ。v3時代にbg-white dark:bg-gray-900みたいな対ペアを全コンポーネントに書いてたのが嘘みたいです。あの「ライト用とダーク用を両方書き忘れないように」という謎の緊張感から解放されたのが個人的にはかなりデカかった。
フローで整理するとこんな感じ。
flowchart TD
A[ユーザーのOS設定 / クラス切り替え] --> B{ダークモード判定}
B -- prefers-color-scheme: dark --> C[CSS変数がダーク値に切り替わる]
B -- .dark クラス付与 --> C
B -- ライトモード --> D[CSS変数がライト値のまま]
C --> E[全コンポーネントが自動的にダーク表示]
D --> F[全コンポーネントがライト表示]
E --> G[transition-colors で滑らかに切り替え]
F --> G
ちなみにNext.js 15 × React 19を本番投入して6ヶ月、キャッシュ設計で痛い目を見た話でも触れたけど、Next.jsのApp RouterとこのCSS変数アプローチはかなり相性がいい。Server ComponentsはCSS変数の切り替えに干渉しないので、ダークモード実装がクライアントコンポーネント汚染しなくて済む。
v3からv4へのマイグレーション、実際にかかったコスト
チームで経験したv3→v4移行の現実を正直に書いておく。うちのプロジェクトは中規模(コンポーネント200個程度)だったんだけど、完全移行まで大体2週間かかった。内訳はこんな感じ。
pie title v3→v4移行コスト内訳(人日)
"tailwind.config.js → @theme 変換" : 2
"プラグインv4対応確認・入れ替え" : 1.5
"削除されたユーティリティの対応" : 3
"チームへの新記法レクチャー" : 1
"回帰テスト・スタイル崩れ修正" : 4.5
"ドキュメント整備" : 1
予想より大変だったのが「削除されたユーティリティの対応」と「スタイル崩れ修正」。v4ではv3で使えた一部のユーティリティが名称変更・削除されていて、公式のUpgrade Guideのcodemodを使っても完全には変換しきれない箇所がいくつかあった。
特にハマったのがshadow-系のユーティリティ。v4でデフォルトの影の強さが微妙に変わっていて、目で見て「なんか影が違う」と気づくまでに時間がかかった。こういう細かい視覚的な変更は自動検出が難しいので、主要ページのスクリーンショット比較を手動でやる羽目になった。Jest・Vitest・Playwrightの使い分け|2026年テスト戦略でも触れているけど、ビジュアルリグレッションテストをPlaywrightで組んでおけばよかったと後悔している。
| 移行の難易度 | 項目 | 備考 |
|---|---|---|
| 低 | @theme への変換 | codemodでほぼ自動 |
| 低 | @import "tailwindcss" への変更 | 単純置換 |
| 中 | プラグイン対応確認 | 2026年時点では主要品は対応済み |
| 高 | 削除・名称変更されたクラスの修正 | 手動確認が必要 |
| 高 | 視覚的な微差のチェック | スクリーンショット比較推奨 |
あと、地味に助かったのがTurborepo 2.x × pnpm WorkspacesでモノレポCI/CDを最適化する完全ガイドでまとめたような構成でビルドキャッシュを活用していたこと。移行中は何度もフルビルドを走らせることになるので、Turborepoのキャッシュがなかったら検証時間がもっとかかっていたと思う。
v4への移行を検討している人に一言アドバイスするなら「codemodを過信しない、視覚的な確認に時間を確保する」です。
まとめ
Tailwind CSS v4を3ヶ月使ってみての正直な感想は「移行コストは想定より大きかったけど、投資する価値はあった」。
要点をまとめると:
tailwind.config.js廃止とCSSファーストの設定は最初戸惑うが、慣れるとCSSとJSの行き来が減って見通しがよくなる- Oxide Engineのビルド速度は数値上でも体感上でも別次元。インクリメンタルビルドが18msは開発体験を本当に変える
- CSS Layersのネイティブ対応で既存CSSとの優先度衝突問題が解決。段階的導入がしやすくなった
- CSS変数ベースのダークモードはv3の
dark:プレフィックス地獄から解放してくれる。これだけでもv4に上げる価値がある - 移行コストは中規模プロジェクトで約2週間。codemodだけで終わらないので視覚的な確認時間を必ず確保する
まずは小さなサイドプロジェクトでv4を試してみることを強くおすすめしたい。本番プロジェクトへの移行前に@themeの書き方とカスタムユーティリティのパターンを手を動かして体感しておくと、本番移行時のスムーズさが全然違う。
皆さんのチームではv4への移行はもう済んでますか?まだv3で粘ってる方、どんな理由で止まってるか気になる。コメントかTwitter/Xで教えてもらえると嬉しいです。