Tailwind CSS v4.1を半年使って正直に書く、設計思想の変化とやらかし集
「元に戻したい」と言い出したチームメンバーもいたあの混乱期から半年。CSS変数ベースの新設計に慣れるまでの実体験と、本番でハマった落とし穴をそのまま書きました。
Tailwind CSS v4系に切り替えたのは去年の12月末で、最初の1〜2週間は正直「別のフレームワークに乗り換えた」くらいの感覚だった。v3まで積み上げてきた「tailwind.config.jsで全部管理する」という思考回路を根本から壊される体験で、チームメンバーの半数が「元に戻したい」と言い出したくらいには混乱した。でも半年使い込んだ今、設計が一段階シンプルになったという実感がある。やらかしたことも含めてリアルに書いていく。
なお、この記事はCSSの設計話が中心で、Next.js 15との統合はReact Server Components完全ガイド2026でも触れているので興味あれば。
v4の設計思想、「設定ファイルが消える」という衝撃
v4で最初に面食らうのは、tailwind.config.jsが原則不要になっていること。代わりに@themeディレクティブでCSS側に直接テーマを定義する設計になっている。
/* globals.css */
@import "tailwindcss";
@theme {
--color-brand-500: oklch(65% 0.2 250);
--color-brand-600: oklch(58% 0.22 250);
--font-family-sans: "Inter Variable", sans-serif;
--spacing-18: 4.5rem;
--radius-card: 12px;
}
これだけでbg-brand-500、text-brand-600、rounded-cardが使えるようになる。最初見たとき「え、これだけ?」ってなった。v3ではtailwind.config.jsのextendに書いていたものが、そのままCSS変数として機能する。
内部的には、v4からビルドエンジンがRustベースの@tailwindcss/oxideに刷新されていて、ビルド速度が劇的に改善している。うちのプロジェクト(コンポーネント数が約400)だと、フルビルドが以前の8秒前後から1.2秒まで落ちた。
xychart-beta
title "Tailwind CSSビルド時間比較(400コンポーネント構成)"
x-axis ["v3.4 (full)", "v3.4 (incremental)", "v4.1 (full)", "v4.1 (incremental)"]
y-axis "ビルド時間 (秒)" 0 --> 10
bar [8.2, 1.8, 1.2, 0.15]
インクリメンタルビルドは0.15秒。HMRのラグがほぼゼロになった感覚があって、これは地味に便利だった。
もう一つ大きな変化が、カラー管理がoklchに移行したこと。v3はhexやrgbで定義していたが、v4のデフォルトカラーパレットはすべてoklchベースに変わっている。メリットは知覚的均一性で、brand-500とbrand-600の「暗さの差」が人間の目に対して均一になる。デザイナーと話すとき、「この色もう少し暗くして」という指示が定量的に扱えるようになったのは個人的に地味に嬉しかった。
ただし落とし穴があって、古いブラウザ(2023年以前のSafari等)ではoklchが非対応のため、@supportsでフォールバックが必要になるケースがある。本番のユーザーエージェント分析をちゃんとやっておくこと。うちは対象外だったので問題なかったが、BtoCのサービスだと確認必須だと思う。
実際の移行作業でハマったこと
v3からv4への移行は「それなりに痛い」というのが正直なところ。公式の@tailwindcss/upgradeコマンドはあるし、機械的な変換はかなりやってくれる。
npx @tailwindcss/upgrade@next
ただ、これで全部解決するわけじゃない。実際に引っかかったポイントをまとめるとこんな感じだ:
| 変更点 | v3 | v4.1 | 対応コスト |
|---|---|---|---|
| 設定ファイル | tailwind.config.js | @theme in CSS | 中 |
| JIT | デフォルト有効 | 常時有効(変更不可) | 低 |
ringのデフォルト幅 | 3px | 1px | 高(見た目バグる) |
shadowのカラー | rgb(0,0,0) | currentColor | 高(ダークモード) |
borderのデフォルト幅 | 1px | なし(要明示) | 中 |
| プレフィックス記法 | dark: | 同じ(変更なし) | なし |
@applyの動作 | 一部制限あり | さらに制限強化 | 中〜高 |
ringのデフォルト幅が3px→1pxになっているのが思わぬところで引っかかった。フォームのフォーカス状態が全部細くなって、アクセシビリティ担当から「フォーカス見えない」と報告が来た。ring-2に全部書き換えるのに丸半日かかった。アクセシビリティ周りは本当に地味に響くので、アクセシビリティを後付けした僕たちの失敗も参照してほしい。
shadowのカラー変更はダークモード実装をしているプロダクトで特に痛い。v3まではshadowが黒固定だったのが、v4からはcurrentColorを参照するようになった。ダークモードのテキストカラーが白系の場合、影が白くなるという想定外の見た目になる。これ、最初は何が起きているのか全然わからなくて、Chromeのdevtoolsと30分格闘した。
/* v4でダークモードのshadowを明示的に制御 */
@layer base {
:root {
--shadow-color: 0deg 0% 15%;
}
.dark {
--shadow-color: 0deg 0% 5%;
}
}
こういうの、移行前にチェックリストとして整理しておくべきだった。反省。
v4.1の新機能、実務で使えるやつとそうじゃないやつ
v4.1(2026年2月リリース)でいくつか便利な機能が追加されている。半年使って「これは実務で使える」と判断したものだけ書く。
@variantディレクティブ
カスタムバリアントをCSSで定義できるようになった。v3だとJSの設定でpluginを書く必要があったが:
@variant hocus (&:hover, &:focus);
@variant can-hover (@media (hover: hover));
これだけでhocus:bg-brand-500みたいな書き方ができる。インタラクション系のスタイルがスッキリした。地味に便利。
field-sizing ユーティリティ
<textarea class="field-sizing-content" />
テキストエリアの高さが入力内容に応じて自動調整される。今まで自前でJSで実装していたやつがゼロコードになった。これはマジで助かった。
not-* バリアント
<li class="not-last:border-b border-gray-200">
:not(:last-child)の省略形。space-y-*を使わずに区切り線を実装するパターンが読みやすくなった。
一方で、@applyはv4.1でさらに制限が強化された。v3でも「乱用するな」とドキュメントに書いてあったけど、v4.1では一部の動的クラスで使えないケースが増えた。
/* これはv4.1でエラーになるケースがある */
.btn {
@apply bg-[var(--color-primary)] text-white; /* 動的なCSS変数参照はNG */
}
/* こうする */
.btn {
background-color: var(--color-primary);
@apply text-white;
}
コンポーネントライブラリを自前で持っているプロジェクトだと、@applyへの依存度が高いことがある。うちは既存の.btnや.form-inputみたいなユーティリティクラスを40個くらい持っていて、そのうち7個が修正必要だった。数としては多くないが、どれが壊れているか見つけ出す作業が地味に時間を食った。
CSS変数ベース設計の実践パターン
v4に移行して一番良かったのは、デザイントークンとTailwindが自然に統合されること。v3だと「デザインシステムのCSS変数」と「Tailwindのconfig」の2箇所を管理していたが、v4ではCSS変数がそのままユーティリティになる。
flowchart LR
subgraph v3["v3の管理(2箇所)"]
A["design-tokens.css\nCSS変数定義"]
B["tailwind.config.js\nテーマ設定"]
C["コンポーネント"]
A --> C
B --> C
end
subgraph v4["v4の管理(1箇所)"]
D["globals.css\n@theme定義"]
E["コンポーネント"]
D --> E
end
うちのチームではデザイナーがFigma Variablesでトークンを管理していて、それをエクスポートしてCSS変数に変換→Tailwindユーティリティとして使うフローを組んでいる。
/* figma-tokens.cssから自動生成 */
@theme {
/* Semantic tokens */
--color-surface-default: var(--color-gray-50);
--color-surface-elevated: var(--color-white);
--color-surface-overlay: var(--color-gray-900);
--color-interactive-primary: var(--color-brand-500);
--color-interactive-primary-hover: var(--color-brand-600);
--color-interactive-primary-disabled: var(--color-gray-300);
/* Dark mode overrides */
--color-surface-default: light-dark(var(--color-gray-50), var(--color-gray-900));
}
light-dark()関数との組み合わせが特に強力で、ダークモードの切り替えをCSS変数一本で管理できる。v3時代にdark:プレフィックスを100個書き散らしていたのが馬鹿みたいに感じた。
実際のパフォーマンス計測もしてみた:
xychart-beta
title "本番バンドルサイズ比較 (gzip後)"
x-axis ["v3.4構成", "v4.1構成", "v4.1+最適化後"]
y-axis "サイズ (KB)" 0 --> 40
bar [32.4, 18.7, 12.3]
バンドルサイズが顕著に小さくなった。v4はPurgeの最適化がさらに改善されていて、実際に使われているユーティリティだけが出力される精度が上がっている。32KBから12KBはかなり効いている。
正直、まだ迷っている部分
ここまで良い話ばかり書いたけど、実際のところ「まだ判断がついていない」部分もある。
tailwind.config.js完全廃止の是非
v4は設定ファイル不要を推しているが、大規模プロジェクトでCSS側だけで管理するのが本当に良いのか、正直まだ検証中。Plugins APIの一部はまだJSで書く必要があるし(特にTypography pluginを自前でカスタマイズしているケース)、JSでロジックが書けた方が便利な場面はある。CSSに全部書けば統一感は出るけど、型安全性がなくなるトレードオフは無視できない。
Scoped stylesとの共存
モノレポ運用ガイド2026年版でも触れているが、モノレポ構成でパッケージをまたいでTailwindの設定を共有する場合、@themeをどのレイヤーで定義するかがまだ確立していない。うちはpackages/design-tokens/src/theme.cssに集約して各パッケージからimportする構成にしたが、Turborepoのキャッシュとの相性でたまに意図しないリビルドが走る。
皆さんのチームはどういう構成にしてます?
shadcn/ui v3との互換性
shadcn/uiがTailwind v4対応を進めているが(2026年4月時点でベータ)、既存のv3ベースのコンポーネントとの混在が地味に辛い。特にhslカラー変数とoklchの混在で、Storybook上の見た目がズレるケースが何度かあった。これは早く安定版が出てほしい。
まとめ
半年使って、Tailwind CSS v4.1への移行は「やって良かった」というのが総合的な評価。ただし移行コストは想定の1.5〜2倍かかると思っておいた方が良い。
| 観点 | 内容 |
|---|---|
@themeディレクティブ | 設計がシンプルになる。デザイントークンとTailwindの二重管理から解放される |
ring・shadowのデフォルト変更 | 見落としやすい。移行前に必ずビジュアルリグレッションテストを回す |
| ビルド速度 | フルビルド8秒→1.2秒、インクリメンタル0.15秒。DXに直結する |
oklchカラー | デザイナーとの協業がしやすくなるが、ターゲットブラウザのサポート確認は必須 |
@apply依存が高い既存コード | 移行コストが高い。事前の棚卸しを推奨 |
既存プロジェクトで試す場合は、まず新規ページか新規コンポーネントだけをv4で作るハイブリッド構成から始めるのをお勧めする。npx @tailwindcss/upgradeで一気にやろうとすると、上に書いたような落とし穴を全部踏む羽目になる。段階的に移行しながら設計パターンを積み上げていく方が精神的に安全だし、チームの理解度も追いつく。
特に既存記事のTailwind CSS v4を本番投入して3ヶ月でも移行初期の混乱を書いているので、そちらも合わせて読んでもらえると文脈が伝わりやすいと思う。