Tailwind CSS v4.1を半年本番で使って痛感した設計思想の変化と落とし穴

「v3からそんなに変わらんやろ」と思って痛い目を見た話。Oxide Engine・@theme・Lightning CSS統合を実務で半年使い続けて気づいたことを正直に書きます。

先日チームのコードレビューで「このクラス名、何やってるか全然わからん」ってコメントをもらって、改めてTailwindとの向き合い方を見直す機会があった。うちのチームはTailwind CSS v4.1を去年末から本番投入していて、ちょうど半年が経つ。正直「v3から移行したらそんなに変わらんやろ」と思っていたんだけど、実際に触るとエンジンレベルから設計思想まで根本的に変わっていて、最初の1ヶ月はかなり痛い目を見た。

というわけで、実務で得た知見を整理しておく。Tailwind v4を検討しているチームの参考になれば。


v4で何が変わったか——Oxide Engineと設定ファイルレス化の本当の意味

v4の最大の変更点はやっぱりOxide Engineの採用と、tailwind.config.jsが原則不要になったことだと思う。これがどういう意味かというと、設定はすべてCSSファイル内の@themeディレクティブで完結するようになった。

/* globals.css */
@import "tailwindcss";

@theme {
  --color-brand-primary: #3b82f6;
  --color-brand-secondary: #10b981;
  --font-family-sans: 'Inter Variable', sans-serif;
  --spacing-18: 4.5rem;
  --radius-card: 12px;
}

これだけでbg-brand-primaryfont-sansrounded-cardみたいなユーティリティが自動生成される。最初見たとき「え、これだけで動くの?」って半信半疑だったんだけど、実際にビルドして確認したら本当に動いた。

ビルド速度の変化は体感できるレベルで違う。うちの中規模プロジェクト(コンポーネント300個程度)での実測値は以下のとおり。

xychart-beta
  title "Tailwind CSS ビルド時間比較(ms)"
  x-axis ["初回ビルド", "インクリメンタル", "本番ビルド"]
  y-axis "ビルド時間 (ms)" 0 --> 4000
  bar [3200, 850, 3800]
  bar [480, 42, 620]

上がv3、下がv4の数値。インクリメンタルビルドが850ms→42msというのは地味にデカくて、開発中のホットリロードが体感でかなり速くなった。個人的にはこの差が一番うれしかったかもしれない。ファイル保存してからブラウザが更新されるまでの微妙なラグがなくなると、開発のテンポが違う。

一方で移行時にハマったのが、Lightning CSS統合によるPostCSS設定の変化だった。v4はLightning CSSをデフォルトで使うので、postcss.config.jsでautoprefixerを別途設定していた場合は競合する。うちのチームはCSSの変換周りを結構カスタマイズしていたので、ここで半日ほど溶かした。

// v3時代のpostcss.config.js(要注意)
module.exports = {
  plugins: [
    require('tailwindcss'),
    require('autoprefixer'), // v4では不要になる
  ],
};

// v4対応後
module.exports = {
  plugins: {
    '@tailwindcss/postcss': {}, // これだけでOK
  },
};

この辺はNext.js 15 × React 19を本番投入して6ヶ月、キャッシュ設計で痛い目を見た話でも似たような設定周りのつまずきが紹介されているけど、フレームワーク側のデフォルト設定との衝突は本当に地味にしんどい。


@variantと@applyの使い所——ユーティリティファーストを崩さない書き方

v4で地味に便利になったのが@variantディレクティブ。カスタムバリアントをCSSで定義できるようになった。

@variant hocus {
  &:hover,
  &:focus-visible {
    @slot;
  }
}

@variant sidebar-open {
  .sidebar-open & {
    @slot;
  }
}

これを使うとhocus:underlineとかsidebar-open:blockみたいな書き方ができる。グローバルな状態に依存したスタイルをコンポーネント側で宣言的に書けるようになったのはかなり実用的だと感じた。状態管理とスタイルの責務がスッキリ分離できるので、特にサイドバーの開閉みたいな「親要素の状態を子が参照するパターン」で重宝している。

ただし@applyの使いすぎは相変わらず注意が必要だ。チームでよく議論になるのが「どこまでコンポーネントに抽出してどこまでインラインで書くか」という問題で、うちではだいたい以下の方針で運用している。

ケース方針理由
単一コンポーネント専用スタイルインラインclassで書く変更範囲が明確
3箇所以上で同じパターンが登場コンポーネント化を検討@applyより再利用性が高い
ベーススタイル(resetやtypography)@applyをCSSレイヤーに書く意図が伝わりやすい
プラグインとして提供したいスタイルplugin()経由で定義ツールチェーンとの一貫性

ここは正直まだチーム内で統一しきれていない部分もあって、特に「カードコンポーネントの影スタイルをどこに書くか」で毎回議論になる。好みが分かれる部分だし、正解は状況次第かもしれない。

v4のもう一つの改善点として、動的クラス名のサポートが強化された。v3ではPurgeCSSの都合上、テンプレートリテラルで動的にクラス名を組み立てると消えることがあった。

// v3時代:これは動かないケースがあった
const colorClass = `bg-${color}-500`;

// v4:コンテンツ検出が賢くなって動くケースが増えたが...
// 安全のためcn()やclsx()でホワイトリスト管理が依然推奨
import { cn } from '@/lib/utils';

const Button = ({ variant = 'primary' }: { variant: 'primary' | 'danger' }) => {
  const variantClasses = {
    primary: 'bg-brand-primary hover:bg-brand-primary/90',
    danger: 'bg-red-500 hover:bg-red-600',
  };
  
  return (
    <button className={cn('px-4 py-2 rounded-card text-white', variantClasses[variant])}>
      クリック
    </button>
  );
};

動的クラス名問題はv4でも完全に解決したわけじゃないので、clsxcva(class-variance-authority)との組み合わせは依然として有効。むしろこのパターンを最初から徹底しておいたほうが、後で困らないと思う。


チームで半年運用してわかった、設計ルールとコードレビューの実務

Tailwindを複数人で運用するとき一番困るのは「クラス名の書く順序がバラバラになる」問題だと思う。flex items-center justify-betweenを書く人もいればjustify-between flex items-centerと書く人もいて、レビューでいちいち指摘するのも正直しんどい。

これはPrettierプラグインで解決できる。

// .prettierrc
{
  "plugins": ["prettier-plugin-tailwindcss"],
  "tailwindConfig": "./tailwind.config.ts",
  "tailwindFunctions": ["cn", "clsx", "cva"]
}

これを入れてからCIでprettier --checkを走らせるようにしたら、クラス順序に関するレビューコメントがほぼゼロになった。マジで助かった。「なんでこの順番なの?」みたいな議論がそもそも発生しなくなるので、レビューの時間をもっと本質的なところに使えるようになったのが地味に大きい。

次に課題になったのがダークモードの実装方針だった。v4では@theme内でCSSカスタムプロパティを活用したカラーシステムが構築しやすくなっている。

@theme {
  --color-surface: #ffffff;
  --color-surface-elevated: #f8fafc;
  --color-text-primary: #0f172a;
  --color-text-secondary: #64748b;
}

@media (prefers-color-scheme: dark) {
  @theme {
    --color-surface: #0f172a;
    --color-surface-elevated: #1e293b;
    --color-text-primary: #f1f5f9;
    --color-text-secondary: #94a3b8;
  }
}

この方法でセマンティックなカラートークンを定義しておくと、bg-surfacetext-text-primaryが自動でダークモード対応になる。dark:bg-gray-900みたいに明示的なダーククラスをべたべた書かなくて済むので、コードがスッキリする。

ただしこのアプローチ、既存プロジェクトへの導入は結構コストがかかる。うちは新機能部分から段階的に適用している段階で、全体の統一はまだ途中だ。カラートークンの命名規則で一悶着あったりして、「設計の議論」に思ったより時間を取られた。でも長期的には絶対やっておくべき投資だと思っている。

コンポーネントライブラリとの相性も確認しておく必要がある。うちが使っている構成をまとめるとこうなる。

ライブラリv4対応状況実際の使用感
shadcn/ui対応済み(2025年末〜)問題なし。@theme変数との統合がきれい
Radix UI対応済みCSSレイヤー周りで若干調整が必要だった
Headless UIv2.2で対応基本的に問題なし
React Aria対応済み特に問題なし
daisyUIv5でv4対応プラグイン形式が変わったので要注意

shadcn/uiはv4対応後に@theme変数ベースのカラーシステムになって、カスタマイズがかなりしやすくなった。モノレポ運用ガイド|2026年ベストプラクティスと導入戦略で紹介しているようなTurborepo構成でUIパッケージを分離している場合も、この変数ベースの設計なら上書きしやすくて相性がいい。


パフォーマンス最適化——Tailwind v4で変わったバンドルサイズの考え方

v4からOxide Engineになってコンテンツ検出が賢くなり、使われていないユーティリティの除去精度が上がった。実際のバンドルサイズの変化はこんな感じ。

xychart-beta
  title "本番CSSバンドルサイズ比較 (KB, gzip後)"
  x-axis ["LP", "管理画面", "ECサイト", "ダッシュボード"]
  y-axis "ファイルサイズ (KB)" 0 --> 30
  bar [18, 24, 28, 26]
  bar [11, 15, 17, 14]

上がv3、下がv4。平均で約40%削減できている。特に管理画面系のコンポーネントが多いプロジェクトで効果が出やすかった。数字だけ見るとそこまで劇的じゃないかもしれないけど、モバイル回線でのFirst Loadに効いてくるので、地道にやる価値はある。

ただしここで注意点がある。v4ではContent Sourcesの指定がtailwind.config.jsではなくPostCSS/Viteプラグインの設定、あるいはCSSファイル内の@sourceディレクティブで行うようになった。

/* 特定ディレクトリを追加でスキャンする場合 */
@source "../../packages/ui/src";
@source "./node_modules/some-ui-library/dist";

モノレポ構成でUIパッケージが別ディレクトリにある場合は@sourceを明示的に指定しないとクラスが消えてしまう。これに気づくのに1日かかった。本番環境でだけスタイルが当たらない状態になるやつで、原因の特定が地味につらかった。

v4から@layerの扱いも変わっている。v4ではCSSカスケードレイヤー(@layer)をネイティブに活用するようになり、basecomponentsutilitiesがブラウザのネイティブカスケードレイヤーとして機能する。

/* v4での正しい書き方 */
@layer components {
  .card {
    background: var(--color-surface-elevated);
    border-radius: var(--radius-card);
    padding: var(--spacing-6);
    box-shadow: var(--shadow-md);
  }
}

これの何が嬉しいかというと、ユーティリティクラスが常にコンポーネントより優先されることが保証されるので、!importantを使わずにオーバーライドできる場面が増えた。アクセシビリティを後付けした僕たちの失敗──3年のWebチーム実装記でも触れているような「後付けスタイルの優先度地獄」を回避しやすくなるのは実務的にデカい。

Viteを使っているプロジェクトであれば、Viteプラグインの導入が最も簡単でパフォーマンスも出やすい。

// vite.config.ts
import { defineConfig } from 'vite';
import tailwindcss from '@tailwindcss/vite';

export default defineConfig({
  plugins: [
    tailwindcss(),
  ],
});

PostCSS経由より起動が速いので、新規プロジェクトならViteプラグイン一択でいいと思う。Next.js 15を使っている場合はPostCSS経由になるが、差は体感しにくい。


v3からの移行戦略——一括移行は地雷、段階移行が正解

v3からv4への移行を検討しているチームに伝えておきたいのは、一括移行は基本的に推奨しないということ。破壊的変更がそれなりにあって、特に以下の点は要注意だ。

flowchart TD
  A[v3プロジェクト] --> B{移行前チェック}
  B --> C[tailwind.config.js の
theme.extend 内容を確認]
  B --> D[カスタムプラグインの
互換性確認]
  B --> E[PostCSS設定の
autoprefixer競合確認]
  B --> F[動的クラス名の
パターン棚卸し]
  C --> G[@theme ディレクティブに
書き直す]
  D --> H[プラグインAPIが
変わっているので要書き直し]
  E --> I[@tailwindcss/postcss
に一本化]
  F --> J[cva等でホワイトリスト管理
に移行]
  G --> K[段階的に移行完了]
  H --> K
  I --> K
  J --> K

特にハマりがちなのはカスタムプラグインのAPI変更だ。v3でplugin()を使っていたプラグインは大体動くが、matchComponents()matchUtilities()の引数が微妙に変わっているものがあって、TypeScriptを使っていると型エラーでどこが壊れているか気づきやすいが、JSだけだとランタイムで初めて気づくこともある。TypeScriptで書いておいてよかったと思った瞬間のひとつだった。

うちのチームが取ったアプローチは「新しいページ・機能からv4で書いて、既存部分は触るときに少しずつ移行する」というもの。Next.js App RouterのRoute単位でCSSをスコープできることもあって、コンポーネントレベルで段階的に移行しやすかった。

主要な破壊的変更をまとめるとこうなる。

v3の書き方v4の対応備考
tailwind.config.jsのtheme@theme {}ディレクティブCSS内で完結するように
theme('colors.blue.500')in CSSvar(--color-blue-500)CSS変数として参照
@applyの多用基本変わらずだが非推奨傾向コンポーネント化を推奨
autoprefixerのセット不要(Lightning CSS内蔵)postcss.config.jsの変更が必要
content: []の設定自動検出 + @source基本は自動で動く
safelistの設定@source inline(...)動的クラスの保護方法が変わった
dark:バリアント設定@variant darkで自由に定義より柔軟になった

自動移行ツール(npx @tailwindcss/upgrade)も存在するが、カスタマイズが多いプロジェクトだと手動での確認が結局必要になる。ツールで8割変換して、残り2割を手で直すイメージで使うのが現実的。完全に任せようとすると痛い目を見るので、あくまで補助として使うのが正解だと思う。


まとめ

Tailwind CSS v4を半年本番運用してみて、正直「乗り換えてよかった」という結論に落ち着いている。最初の移行コストは確かにかかるけど、ビルド速度とバンドルサイズの改善は地味に効いてくる。

要点をまとめると:

  1. Oxide Engineによるビルド高速化は体感できるレベル。インクリメンタルビルドが特に速くなって、開発体験が改善した。ただし移行時はPostCSS設定の競合に注意。

  2. @themeディレクティブによる設定のCSSファイル完結は設計思想の転換で、最初は戸惑うが慣れると直感的。セマンティックカラートークンを定義すればダークモード対応もシンプルになる。

  3. モノレポ・マルチパッケージ構成では@sourceの明示指定が必須。ここを見落とすと本番でクラスが消えるという地味で辛いバグが発生する。

  4. 一括移行より段階移行が安全npx @tailwindcss/upgradeは補助ツールとして使いつつ、カスタムプラグインやPostCSS設定は手動で確認する。

  5. prettier-plugin-tailwindcssは即導入すべき。クラス順序の統一はコードレビューのノイズを確実に減らしてくれる。

次のアクション: まずステージング環境でv4のブランチを切って、@tailwindcss/upgradeを走らせてみるところから始めるといい。移行にかかる工数は、カスタマイズ量にもよるけど小〜中規模プロジェクトなら1〜3日みておけば大体いけると思う。まだv3のままのチームも多いと思うけど、Next.js 15への移行タイミングと合わせて検討してみる価値はあると思う。

U

Untanbaby

ソフトウェアエンジニア|AWS / クラウドアーキテクチャ / DevOps

10年以上のIT実務経験をもとに、現場で使える技術情報を発信しています。 記事の誤りや改善点があればお問い合わせからお気軽にご連絡ください。

関連記事