コンテンツへスキップ
·5分で読めます

Tailwind CSS v4:実際に何が変わったのか、移行すべきか

CSSファースト設定、@layerとの統合、組み込みコンテナクエリ、新エンジンのパフォーマンス、破壊的変更、そしてv3からv4への正直な移行体験。

シェア:X / TwitterLinkedIn

Tailwind CSSはv1.xの頃から使っています。コミュニティの半分が「邪道だ」と思い、残りの半分が「これなしでは開発できない」と感じていた時代からです。メジャーバージョンアップのたびに大きな飛躍がありましたが、v4は異質です。単なる機能リリースではありません。フレームワークとあなたの間の根本的な契約を変える、一からのアーキテクチャ書き換えです。

v3からv4へ2つのプロダクションプロジェクトを移行し、v4でゼロから3つの新規プロジェクトを立ち上げた結果、何が本当に良くなったか、何が荒削りか、今日移行すべきかについて明確な見解を持っています。誇大広告も怒りもなし——私が観察したことだけを伝えます。

全体像:v4とは何か#

Tailwind CSS v4は同時に3つのものです:

  1. 新しいエンジン — JavaScriptからRustに書き換えられ(Oxideエンジン)、ビルドが劇的に高速化
  2. 新しい設定パラダイム — CSSファースト設定がデフォルトとしてtailwind.config.jsを置き換え
  3. CSSプラットフォームとのより緊密な統合 — ネイティブ@layer、コンテナクエリ、@starting-style、カスケードレイヤーがファーストクラスの市民に

どこでも目にする見出しは「10倍高速」です。これは本当ですが、実際の変化を過小評価しています。Tailwindの設定と拡張に関するメンタルモデルが根本的に変わりました。もはやCSSを生成するJavaScript設定オブジェクトではなく、CSS自体で作業するようになったのです。

最小限のTailwind v4セットアップはこのようになります:

css
/* app.css — セットアップの全体 */
@import "tailwindcss";

これだけです。設定ファイルなし。PostCSSプラグイン設定もなし(ほとんどのセットアップで)。@tailwind base; @tailwind components; @tailwind utilities;ディレクティブもなし。1つのインポートで動作します。

v3と比較してみましょう:

css
/* v3 — app.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
js
// v3 — tailwind.config.js
/** @type {import('tailwindcss').Config} */
module.exports = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx,mdx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
};
js
// v3 — postcss.config.js
module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  },
};

3つのファイルが1行に削減されました。これは単にボイラープレートが減っただけではなく、設定ミスの可能性が減ったということです。v4ではコンテンツ検出が自動化されています。globパターンを指定しなくても、プロジェクトファイルを自動的にスキャンします。

@themeによるCSSファースト設定#

これが最大の概念的転換です。v3では、JavaScript設定オブジェクトを通じてTailwindをカスタマイズしていました:

js
// v3 — tailwind.config.js
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          50: "#eff6ff",
          500: "#3b82f6",
          900: "#1e3a5f",
        },
      },
      fontFamily: {
        display: ["Inter Variable", "sans-serif"],
      },
      spacing: {
        18: "4.5rem",
        112: "28rem",
      },
      borderRadius: {
        "4xl": "2rem",
      },
    },
  },
};

v4では、これらすべてが@themeディレクティブを使ってCSSに配置されます:

css
@import "tailwindcss";
 
@theme {
  --color-brand-50: #eff6ff;
  --color-brand-500: #3b82f6;
  --color-brand-900: #1e3a5f;
 
  --font-display: "Inter Variable", sans-serif;
 
  --spacing-18: 4.5rem;
  --spacing-112: 28rem;
 
  --radius-4xl: 2rem;
}

最初は抵抗感がありました。デザインシステム全体を一望できる単一のJavaScriptオブジェクトが気に入っていたのです。しかし、CSSアプローチを1週間使ってみて、3つの理由で考えが変わりました:

1. ネイティブCSSカスタムプロパティが自動的に公開されます。 @themeで定義したすべての値が:rootのCSSカスタムプロパティになります。つまり、テーマ値は素のCSS、CSSモジュール、<style>タグなど、CSSが動作するあらゆる場所でアクセス可能になります:

css
/* 無料で手に入ります */
:root {
  --color-brand-50: #eff6ff;
  --color-brand-500: #3b82f6;
  --color-brand-900: #1e3a5f;
}
css
/* どこでも使えます — Tailwind不要 */
.custom-element {
  border: 2px solid var(--color-brand-500);
}

2. @theme内でCSS機能を使えます。 メディアクエリ、light-dark()calc() — 本物のCSSがここで動きます。なぜなら、これは本物のCSSだからです:

css
@theme {
  --color-surface: light-dark(#ffffff, #0a0a0a);
  --color-text: light-dark(#0a0a0a, #fafafa);
  --spacing-container: calc(100vw - 2rem);
}

3. 他のCSSとの共存。 テーマ、カスタムユーティリティ、ベーススタイルがすべて同じ言語で、望めば同じファイルに配置できます。「CSSの世界」と「JavaScript設定の世界」の間でコンテキストスイッチする必要がありません。

デフォルトテーマのオーバーライドと拡張#

v3ではtheme(置換)とtheme.extend(マージ)がありました。v4ではメンタルモデルが異なります:

css
@import "tailwindcss";
 
/* デフォルトテーマを拡張 — 既存のカラーと並んでブランドカラーを追加 */
@theme {
  --color-brand-500: #3b82f6;
}

名前空間を完全に置き換えたい場合(全デフォルトカラーの削除など)は、@theme--color-*ワイルドカードリセットを使います:

css
@import "tailwindcss";
 
@theme {
  /* まずすべてのデフォルトカラーをクリア */
  --color-*: initial;
 
  /* 次に自分のカラーだけを定義 */
  --color-white: #ffffff;
  --color-black: #000000;
  --color-brand-50: #eff6ff;
  --color-brand-500: #3b82f6;
  --color-brand-900: #1e3a5f;
}

このワイルドカードリセットパターンはエレガントです。デフォルトテーマのどの部分を残し、どの部分を置き換えるかを正確に選べます。デフォルトのspacingは全部残してカラーだけカスタムにしたい?--color-*: initial;をリセットして、spacingはそのままにします。

複数テーマファイル#

大規模プロジェクトでは、テーマをファイルに分割できます:

css
/* styles/theme/colors.css */
@theme {
  --color-brand-50: #eff6ff;
  --color-brand-100: #dbeafe;
  --color-brand-200: #bfdbfe;
  --color-brand-300: #93c5fd;
  --color-brand-400: #60a5fa;
  --color-brand-500: #3b82f6;
  --color-brand-600: #2563eb;
  --color-brand-700: #1d4ed8;
  --color-brand-800: #1e40af;
  --color-brand-900: #1e3a5f;
  --color-brand-950: #172554;
}
 
/* styles/theme/typography.css */
@theme {
  --font-display: "Inter Variable", sans-serif;
  --font-body: "Source Sans 3 Variable", sans-serif;
  --font-mono: "JetBrains Mono Variable", monospace;
 
  --text-display: 3.5rem;
  --text-display--line-height: 1.1;
  --text-display--letter-spacing: -0.02em;
}
css
/* app.css */
@import "tailwindcss";
@import "./theme/colors.css";
@import "./theme/typography.css";

これはv3の巨大なtailwind.config.jsを持つパターンや、require()で分割しようとするパターンよりもはるかにクリーンです。

Oxideエンジン:本当に10倍速い#

Tailwind v4のエンジンはRustで完全に書き換えられました。Oxideと呼ばれています。「10倍速い」という主張には懐疑的でした——マーケティングの数字が実際のプロジェクトで通用することは稀です。そこで自分でベンチマークを取りました。

テストプロジェクト: 847コンポーネント、142ページ、約23,000のTailwindクラス使用箇所を持つNext.jsアプリ。

指標v3 (Node)v4 (Oxide)改善
初回ビルド4,280ms387ms11倍
インクリメンタル(1ファイル編集)340ms18ms19倍
フルリビルド(クリーン)5,100ms510ms10倍
開発サーバー起動3,200ms290ms11倍

「10倍」の主張は私のプロジェクトでは控えめな数字でした。インクリメンタルビルドが真に輝く部分です——18msということは、本質的に瞬時です。ファイルを保存すると、タブを切り替える前にブラウザに新しいスタイルが反映されています。

なぜこれほど速いのか?#

3つの理由があります:

1. JavaScriptの代わりにRust。 コアのCSSパーサー、クラス検出、コード生成がすべてネイティブRustです。「楽しいからRustで書き換えよう」という話ではありません——CSSのパースは本当にCPUバウンドな作業であり、ネイティブコードはV8に対して圧倒的な優位性を持ちます。

2. ホットパスにPostCSSがない。 v3では、TailwindはPostCSSプラグインでした。すべてのビルドで、CSSをPostCSS ASTにパースし、Tailwindプラグインを実行し、CSS文字列にシリアライズし直し、その後他のPostCSSプラグインが実行されていました。v4では、Tailwind独自のCSSパーサーがソースから出力に直接変換します。PostCSSは互換性のためにサポートされていますが、プライマリパスは完全にスキップします。

3. よりスマートなインクリメンタル処理。 新しいエンジンは積極的にキャッシュします。1つのファイルを編集すると、そのファイルだけをクラス名のために再スキャンし、変更されたCSSルールだけを再生成します。v3のエンジンはこの点について一般に思われているより賢かった(JITモードは既にインクリメンタルでした)が、v4は細粒度の依存関係追跡でさらに先を行っています。

速度は本当に重要か?#

はい、ただし想像とは違う理由で重要です。ほとんどのプロジェクトでは、v3のビルド速度は「問題ない」レベルでした。開発中に数百ミリ秒待つ程度。苦痛ではありません。

v4の速度が重要なのは、ツールチェーンの中でTailwindを透明にしてくれるからです。ビルドが20ms以下になると、Tailwindをビルドステップとして意識しなくなります。シンタックスハイライトのように——常にそこにあるけど、邪魔にならない。この心理的な違いは、1日の開発を通じて大きな意味を持ちます。

ネイティブ@layer統合#

v3では、Tailwindは@layer base@layer components@layer utilitiesという独自のレイヤーシステムを使っていました。これらはCSSカスケードレイヤーのように見えましたが、実際にはそうではなく、生成されたCSSが出力内のどこに配置されるかを制御するTailwind固有のディレクティブでした。

v4では、Tailwindは本物のCSSカスケードレイヤーを使用します:

css
/* v4 出力 — 簡略化 */
@layer theme, base, components, utilities;
 
@layer base {
  /* リセット、プリフライト */
}
 
@layer components {
  /* コンポーネントクラス */
}
 
@layer utilities {
  /* 生成されたすべてのユーティリティクラス */
}

これは重要な変更です。なぜなら、CSSカスケードレイヤーには実際の詳細度への影響があるからです。低優先度レイヤーのルールは、セレクターの詳細度に関係なく、高優先度レイヤーのルールに必ず負けます。つまり:

css
@layer components {
  /* 詳細度: 0-1-0 */
  .card { padding: 1rem; }
}
 
@layer utilities {
  /* 詳細度: 0-1-0 — 同じ詳細度だがutilitiesレイヤーが後なので勝つ */
  .p-4 { padding: 1rem; }
}

ユーティリティは常にコンポーネントをオーバーライドします。コンポーネントは常にベースをオーバーライドします。これはv3でも概念的にはTailwindの動作でしたが、今はソース順序の操作ではなく、ブラウザのカスケードレイヤーメカニズムによって強制されます。

カスタムユーティリティの追加#

v3では、プラグインAPIまたは@layer utilitiesでカスタムユーティリティを定義していました:

js
// v3 — プラグインアプローチ
const plugin = require("tailwindcss/plugin");
 
module.exports = {
  plugins: [
    plugin(function ({ addUtilities }) {
      addUtilities({
        ".text-balance": {
          "text-wrap": "balance",
        },
        ".text-pretty": {
          "text-wrap": "pretty",
        },
      });
    }),
  ],
};

v4では、カスタムユーティリティは@utilityディレクティブで定義します:

css
@import "tailwindcss";
 
@utility text-balance {
  text-wrap: balance;
}
 
@utility text-pretty {
  text-wrap: pretty;
}

@utilityディレクティブは「これはユーティリティクラスです——utilitiesレイヤーに配置し、バリアントで使えるようにしてください」とTailwindに伝えます。最後の部分が重要です。@utilityで定義されたユーティリティは、hover:focus:md:、その他すべてのバリアントで自動的に動作します:

html
<p class="text-pretty md:text-balance">...</p>

カスタムバリアント#

@variantでカスタムバリアントも定義できます:

css
@import "tailwindcss";
 
@variant hocus (&:hover, &:focus);
@variant theme-dark (.dark &);
html
<button class="hocus:bg-brand-500 theme-dark:text-white">
  クリックしてください
</button>

これはほとんどのユースケースでv3のaddVariantプラグインAPIを置き換えます。パワーは少し劣ります(プログラマティックなバリアント生成はできません)が、実際に行われることの90%をカバーします。

コンテナクエリ:組み込み、プラグイン不要#

コンテナクエリはv3で最もリクエストの多かった機能の一つでした。@tailwindcss/container-queriesプラグインで利用できましたが、アドオンでした。v4ではフレームワークに組み込まれています。

基本的な使い方#

@containerでコンテナをマークし、@プレフィックスでそのサイズをクエリします:

html
<!-- 親をコンテナとしてマーク -->
<div class="@container">
  <!-- ビューポートではなく親の幅に応答 -->
  <div class="flex flex-col @md:flex-row @lg:grid @lg:grid-cols-3">
    <div class="p-4">カード1</div>
    <div class="p-4">カード2</div>
    <div class="p-4">カード3</div>
  </div>
</div>

@md@lgなどのバリアントはレスポンシブブレークポイントのように機能しますが、ビューポートではなく最も近い@container祖先に対して相対的です。ブレークポイントの値はTailwindのデフォルトブレークポイントに対応します:

バリアント最小幅
@sm24rem (384px)
@md28rem (448px)
@lg32rem (512px)
@xl36rem (576px)
@2xl42rem (672px)

名前付きコンテナ#

特定の祖先をクエリするためにコンテナに名前を付けることができます:

html
<div class="@container/sidebar">
  <div class="@container/card">
    <!-- cardコンテナをクエリ -->
    <div class="@md/card:text-lg">...</div>
 
    <!-- sidebarコンテナをクエリ -->
    <div class="@lg/sidebar:hidden">...</div>
  </div>
</div>

なぜこれが重要か#

コンテナクエリはレスポンシブデザインの考え方を変えます。「このビューポート幅で3カラム表示」ではなく、「このコンポーネントのコンテナが十分な幅になったら3カラム表示」と言えるようになります。コンポーネントが真に自己完結するのです。カードコンポーネントをフル幅レイアウトからサイドバーに移動しても、自動的に適応します。メディアクエリの体操は不要です。

コンポーネントライブラリをビューポートブレークポイントの代わりにデフォルトでコンテナクエリを使うようリファクタリングしてきました。結果として、配置場所に関係なく動作するコンポーネントが生まれ、親がコンポーネントのレスポンシブ動作について何も知る必要がなくなりました。

html
<!-- このコンポーネントは配置されたどんなコンテナにも適応します -->
<article class="@container">
  <div class="grid grid-cols-1 @md:grid-cols-[200px_1fr] gap-4">
    <img
      class="w-full @md:w-auto rounded-lg aspect-video @md:aspect-square object-cover"
      src="/post-image.jpg"
      alt=""
    />
    <div>
      <h2 class="text-lg @lg:text-xl font-semibold">投稿タイトル</h2>
      <p class="mt-2 text-sm @md:text-base text-gray-600">
        投稿の抜粋がここに入ります...
      </p>
      <div class="mt-4 hidden @md:flex gap-2">
        <span class="text-xs bg-gray-100 px-2 py-1 rounded">タグ</span>
      </div>
    </div>
  </div>
</article>

本当に重要な新バリアント#

v4はいくつかの新しいバリアントを追加し、私は常に手を伸ばしています。実際のギャップを埋めてくれます。

starting:バリアント#

これはCSS @starting-styleにマッピングされ、要素が最初に表示される際の初期状態を定義できます。JavaScriptなしで要素の登場をアニメーションする欠けていたピースです:

html
<dialog class="opacity-0 starting:opacity-0 open:opacity-100 transition-opacity duration-300">
  <p>このダイアログは開くとフェードインします</p>
</dialog>

starting:バリアントは@starting-styleブロック内にCSSを生成します:

css
/* Tailwindが生成するもの */
@starting-style {
  dialog[open] {
    opacity: 0;
  }
}
 
dialog[open] {
  opacity: 1;
  transition: opacity 300ms;
}

これはダイアログ、ポップオーバー、ドロップダウンメニューなど、登場アニメーションが必要なもの全般にとって大きな進歩です。以前は次のフレームでクラスを追加するためにJavaScriptが必要だったり、@keyframesを使ったりしていました。今はユーティリティクラスで済みます。

not-*バリアント#

否定。ずっと望んでいたものです:

html
<!-- 最後の子要素以外すべてにボーダーが付く -->
<div class="divide-y">
  <div class="not-last:pb-4">アイテム1</div>
  <div class="not-last:pb-4">アイテム2</div>
  <div class="not-last:pb-4">アイテム3</div>
</div>
 
<!-- 無効でないもののみスタイリング -->
<input class="not-disabled:hover:border-brand-500" />
 
<!-- data属性の否定 -->
<div class="not-data-active:opacity-50">...</div>

nth-*バリアント#

nth-childnth-of-typeへの直接アクセス:

html
<ul>
  <li class="nth-1:font-bold">最初のアイテム — 太字</li>
  <li class="nth-even:bg-gray-50">偶数行 — グレー背景</li>
  <li class="nth-odd:bg-white">奇数行 — 白背景</li>
  <li class="nth-[3n+1]:text-brand-500">3n+1番目 — ブランドカラー</li>
</ul>

ブラケット構文(nth-[3n+1])は有効なnth-child式をすべてサポートします。テーブルのストライピングやグリッドパターンのために以前書いていたカスタムCSSの多くを置き換えます。

in-*バリアント(親の状態)#

これはgroup-*の逆です。「親(group)がホバーされたとき自分をスタイリング」ではなく、「この状態にマッチする親の中にいるとき自分をスタイリング」です:

html
<div class="in-data-active:bg-brand-50">
  いずれかの祖先にdata-activeがあるとき背景が付きます
</div>

**:ディープユニバーサルバリアント#

直接の子要素だけでなく、すべての子孫をスタイリングします。これは制御された力です——控えめに使うべきですが、散文コンテンツやCMS出力には非常に有用です:

html
<!-- このdiv内のすべての段落を、深さに関係なく -->
<div class="**:data-highlight:bg-yellow-100">
  <section>
    <p data-highlight>これがハイライトされます</p>
    <div>
      <p data-highlight>さらに深くネストされたこれも</p>
    </div>
  </section>
</div>

破壊的変更:実際に壊れたもの#

率直に言いましょう。大規模なv3プロジェクトがある場合、移行は簡単ではありません。私のプロジェクトで壊れたものを紹介します:

1. 設定フォーマット#

tailwind.config.jsはそのままでは動きません。以下のいずれかが必要です:

  • @theme CSSに変換する(新アーキテクチャ推奨)
  • 互換レイヤーの@configディレクティブを使う(迅速な移行パス)
css
/* 迅速な移行 — 古い設定を維持 */
@import "tailwindcss";
@config "../../tailwind.config.js";

この@configブリッジは動作しますが、明示的に移行ツールです。長期的には@themeに移行することが推奨されています。

2. 廃止されたユーティリティの削除#

v3で非推奨だったいくつかのユーティリティが削除されました:

/* v4で削除 */
bg-opacity-*     → bg-black/50 を使用(スラッシュ不透明度構文)
text-opacity-*   → text-black/50 を使用
border-opacity-* → border-black/50 を使用
flex-shrink-*    → shrink-* を使用
flex-grow-*      → grow-* を使用
overflow-ellipsis → text-ellipsis を使用
decoration-slice  → box-decoration-slice を使用
decoration-clone  → box-decoration-clone を使用

v3で既にモダンな構文(スラッシュ不透明度、shrink-*)を使っていたなら問題ありません。そうでなければ、これらは単純な検索と置換の変更です。

3. デフォルトカラーパレットの変更#

デフォルトカラーパレットがわずかに変更されました。v3の正確なカラー値(名前ではなく実際のhex値)に依存している場合、視覚的な違いに気付くかもしれません。名前付きカラー(blue-500gray-200)はまだ存在しますが、一部のhex値が変更されています。

4. コンテンツ検出#

v3では明示的なcontent設定が必要でした:

js
// v3
module.exports = {
  content: [
    "./src/**/*.{js,ts,jsx,tsx}",
    "./components/**/*.{js,ts,jsx,tsx}",
  ],
};

v4は自動コンテンツ検出を使用します。プロジェクトルートをスキャンしてテンプレートファイルを自動的に見つけます。これはほとんどの場合「そのまま動きます」が、通常とは異なるプロジェクト構造(プロジェクトルート外にパッケージがあるモノレポ、予期しない場所にあるテンプレートファイル)の場合、ソースパスを明示的に設定する必要があるかもしれません:

css
@import "tailwindcss";
@source "../shared-components/**/*.tsx";
@source "../design-system/src/**/*.tsx";

5. プラグインAPIの変更#

カスタムプラグインを書いていた場合、APIが変更されました。addUtilitiesaddComponentsaddBaseaddVariant関数は互換レイヤーを通じて動作しますが、v4のイディオマティックなアプローチはCSSネイティブです:

js
// v3 プラグイン
plugin(function ({ addUtilities, theme }) {
  addUtilities({
    ".scrollbar-hide": {
      "-ms-overflow-style": "none",
      "scrollbar-width": "none",
      "&::-webkit-scrollbar": {
        display: "none",
      },
    },
  });
});
css
/* v4 — ただのCSS */
@utility scrollbar-hide {
  -ms-overflow-style: none;
  scrollbar-width: none;
  &::-webkit-scrollbar {
    display: none;
  }
}

ほとんどのファーストパーティプラグイン(@tailwindcss/typography@tailwindcss/forms@tailwindcss/aspect-ratio)にはv4互換バージョンがあります。サードパーティプラグインは対応状況がまちまちです——移行前にリポジトリを確認してください。

6. JITのみモード#

v3では、JITモードをオプトアウトできました(ほぼ誰もしていませんでしたが)。v4では非JITモードはありません。すべてが常にオンデマンドで生成されます。何らかの理由で旧AOT(事前コンパイル)エンジンを使っていた場合、その道は閉ざされています。

7. 一部のバリアント構文の変更#

いくつかのバリアントが名称変更または動作変更されました:

html
<!-- v3 -->
<div class="[&>*]:p-4">...</div>
 
<!-- v4 — >*部分がインセットバリアント構文を使用 -->
<div class="*:p-4">...</div>

任意バリアント構文[&...]はまだ動作しますが、v4は一般的なパターンに対して名前付きの代替を提供します。

移行ガイド:実際のプロセス#

実際にどのように移行したかを紹介します。ドキュメントのハッピーパスではなく、プロセスが実際にどのようなものだったかです。

ステップ1:公式コードモッドの実行#

Tailwindはほとんどの機械的な変更を処理するコードモッドを提供しています:

bash
npx @tailwindcss/upgrade

これは多くのことを自動的に行います:

  • @tailwindディレクティブを@import "tailwindcss"に変換
  • 非推奨のユーティリティクラスの名称変更
  • バリアント構文の更新
  • 不透明度ユーティリティをスラッシュ構文に変換(bg-opacity-50bg-black/50に)
  • 設定からの基本的な@themeブロックの作成

コードモッドがうまく処理するもの#

  • ユーティリティクラスの名称変更(ほぼ完璧)
  • ディレクティブ構文の変更
  • シンプルなテーマ値(カラー、spacing、フォント)
  • 不透明度構文の移行

コードモッドが処理しないもの#

  • 複雑なプラグインの変換
  • 動的設定値(JavaScript内のtheme()呼び出し)
  • 条件付きテーマ設定(例:環境に基づくテーマ値)
  • カスタムプラグインAPIの移行
  • 新しいパーサーが異なる解釈をする任意値のエッジケース
  • JavaScriptで動的に構築されるクラス名(テンプレートリテラル、文字列結合)

ステップ2:PostCSS設定の修正#

ほとんどのセットアップでは、PostCSS設定を更新します:

js
// postcss.config.js — v4
module.exports = {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};

注意:プラグイン名がtailwindcssから@tailwindcss/postcssに変更されました。Viteを使っている場合は、PostCSSを完全にスキップしてViteプラグインを使えます:

js
// vite.config.ts
import tailwindcss from "@tailwindcss/vite";
 
export default defineConfig({
  plugins: [tailwindcss()],
});

ステップ3:テーマ設定の変換#

ここが手動作業の部分です。tailwind.config.jsのテーマ値を@themeに変換します:

js
// v3 設定 — 変換前
module.exports = {
  theme: {
    extend: {
      colors: {
        brand: {
          light: "#60a5fa",
          DEFAULT: "#3b82f6",
          dark: "#1d4ed8",
        },
      },
      fontSize: {
        "2xs": ["0.65rem", { lineHeight: "1rem" }],
      },
      animation: {
        "fade-in": "fade-in 0.5s ease-out",
      },
      keyframes: {
        "fade-in": {
          "0%": { opacity: "0" },
          "100%": { opacity: "1" },
        },
      },
    },
  },
};
css
/* v4 CSS — 変換後 */
@import "tailwindcss";
 
@theme {
  --color-brand-light: #60a5fa;
  --color-brand: #3b82f6;
  --color-brand-dark: #1d4ed8;
 
  --text-2xs: 0.65rem;
  --text-2xs--line-height: 1rem;
 
  --animate-fade-in: fade-in 0.5s ease-out;
}
 
@keyframes fade-in {
  0% { opacity: 0; }
  100% { opacity: 1; }
}

キーフレームは@themeの外に出て通常のCSS @keyframesになることに注目してください。@themeのアニメーション名はそれらを参照するだけです。こちらの方がクリーンです——キーフレームはCSSであり、CSSとして書かれるべきです。

ステップ4:ビジュアルリグレッションテスト#

これは必須です。移行後、アプリのすべてのページを開いて視覚的に確認しました。Playwrightのスクリーンショットテストも実行しました(もしあれば)。コードモッドは優秀ですが完璧ではありません。ビジュアルレビューで発見したこと:

  • 不透明度構文の移行がわずかに異なる結果を生成したいくつかの箇所
  • 引き継がれなかったカスタムプラグインの出力
  • レイヤー順序によるz-indexのスタッキング変更
  • カスケードレイヤーで異なる動作をした!importantオーバーライド

ステップ5:サードパーティ依存関係の更新#

Tailwind関連のすべてのパッケージを確認します:

json
{
  "@tailwindcss/typography": "^1.0.0",
  "@tailwindcss/forms": "^1.0.0",
  "@tailwindcss/container-queries": "削除 — 組み込み済み",
  "tailwindcss-animate": "v4サポートを確認",
  "prettier-plugin-tailwindcss": "最新版に更新"
}

@tailwindcss/container-queriesプラグインはもう不要です——コンテナクエリが組み込まれました。他のプラグインにはv4互換バージョンが必要です。

Next.jsとの連携#

ほとんどのプロジェクトでNext.jsを使っているので、具体的なセットアップを紹介します。

PostCSSアプローチ(Next.js推奨)#

Next.jsは内部でPostCSSを使用しているため、PostCSSプラグインが自然な選択です:

bash
npm install tailwindcss @tailwindcss/postcss
js
// postcss.config.mjs
export default {
  plugins: {
    "@tailwindcss/postcss": {},
  },
};
css
/* app/globals.css */
@import "tailwindcss";
 
@theme {
  --font-sans: "Inter Variable", ui-sans-serif, system-ui, sans-serif;
  --font-mono: "JetBrains Mono Variable", ui-monospace, monospace;
}

これがセットアップの全体です。tailwind.config.jsなし、autoprefixerなし(v4がベンダープレフィックスを内部的に処理します)。

CSSインポート順序#

つまずいたポイントが一つ:v4ではカスケードレイヤーのためにCSSインポート順序がより重要になります。@import "tailwindcss"はカスタムスタイルより前に来る必要があります:

css
/* 正しい順序 */
@import "tailwindcss";
@import "./theme.css";
@import "./custom-utilities.css";
 
/* インライン @theme、@utility など */

Tailwindの前にカスタムCSSをインポートすると、スタイルが低いカスケードレイヤーに配置され、予期せず上書きされる可能性があります。

ダークモード#

ダークモードは概念的には同じように動作しますが、設定がCSSに移りました:

css
@import "tailwindcss";
 
/* クラスベースのダークモードを使用(デフォルトはメディアベース) */
@variant dark (&:where(.dark, .dark *));

これはv3の設定を置き換えます:

js
// v3
module.exports = {
  darkMode: "class",
};

@variantアプローチはより柔軟です。ダークモードを好きなように定義できます——クラスベース、data属性ベース、メディアクエリベース:

css
/* data属性アプローチ */
@variant dark (&:where([data-theme="dark"], [data-theme="dark"] *));
 
/* メディアクエリ — デフォルトなので宣言不要 */
@variant dark (@media (prefers-color-scheme: dark));

Turbopack互換性#

Next.jsをTurbopack(現在のデフォルト開発バンドラー)で使っている場合、v4はうまく動作します。Rustエンジンは、Turbopack自身のRustベースアーキテクチャとうまく噛み合います。開発起動時間を計測しました:

セットアップv3 + Webpackv3 + Turbopackv4 + Turbopack
コールドスタート4.8秒2.1秒1.3秒
HMR(CSS変更)450ms180ms40ms

CSS変更に対する40ms HMRはほとんど知覚できません。瞬時に感じます。

パフォーマンス深掘り:ビルド速度を超えて#

Oxideエンジンの利点はビルド速度だけにとどまりません。

メモリ使用量#

v4はメモリ使用量が大幅に少なくなります。847コンポーネントのプロジェクトでの計測:

指標v3v4
ピークメモリ(ビルド)380MB45MB
定常状態(開発)210MB28MB

これはメモリが制限されるCI/CDパイプラインや、10個のプロセスを同時実行する開発マシンにとって重要です。

CSS出力サイズ#

v4は新しいエンジンが重複排除とデッドコード除去に優れているため、わずかに小さいCSS出力を生成します:

v3 出力: 34.2 KB (gzip後)
v4 出力: 29.8 KB (gzip後)

コードを変更せずに13%の削減。革命的ではありませんが、無料のパフォーマンス改善です。

テーマ値のツリーシェイキング#

v4では、テーマ値を定義しても、テンプレートで使用していない場合、対応するCSSカスタムプロパティはそのまま出力されます(@themeに含まれており、:root変数にマッピングされるため)。ただし、未使用値のユーティリティクラスは生成されません。これはv3のJIT動作と同じですが、注目すべき点です:CSSカスタムプロパティは、ユーティリティ使用がない値であっても常に利用可能です。

特定のテーマ値がCSSカスタムプロパティを生成するのを防ぎたい場合は、@theme inlineを使います:

css
@theme inline {
  /* これらの値はユーティリティは生成するがCSSカスタムプロパティは生成しない */
  --color-internal-debug: #ff00ff;
  --spacing-magic-number: 3.7rem;
}

これはCSS変数として公開したくない内部デザイントークンに便利です。

上級編:マルチブランド向けテーマの構成#

v4が大幅に簡単にするパターンの一つが、マルチブランドテーミングです。テーマ値がCSSカスタムプロパティであるため、実行時にスワップできます:

css
@import "tailwindcss";
 
@theme {
  --color-brand: var(--brand-primary, #3b82f6);
  --color-brand-light: var(--brand-light, #60a5fa);
  --color-brand-dark: var(--brand-dark, #1d4ed8);
}
 
/* ブランドオーバーライド */
.theme-acme {
  --brand-primary: #e11d48;
  --brand-light: #fb7185;
  --brand-dark: #9f1239;
}
 
.theme-globex {
  --brand-primary: #059669;
  --brand-light: #34d399;
  --brand-dark: #047857;
}
html
<body class="theme-acme">
  <!-- bg-brand、text-brandなどすべてAcmeカラーを使用 -->
  <div class="bg-brand text-white">Acme Corp</div>
</body>

v3ではこれにカスタムプラグインやTailwind外の複雑なCSS変数設定が必要でした。v4では自然です——テーマはCSS変数であり、CSS変数はカスケードするからです。これがCSSファーストアプローチの正しさを感じさせる類のものです。

v3で恋しいもの#

バランスを取りましょう。v3にあってv4で本当に恋しいものがあります:

1. プログラマティックテーマのためのJavaScript設定。 単一のブランドカラーからJavaScript関数でカラースケールを生成していたプロジェクトがありました。v4では@themeでそれができません——CSSファイルを生成するビルドステップが必要か、カラーを一度計算してペーストする必要があります。@config互換レイヤーは役立ちますが、長期的な解決策ではありません。

2. IntelliSenseはリリース時の方が優秀でした。 v3のVS Code拡張機能は何年もの磨きがかかっていました。v4のIntelliSenseは動作しますが、初期にはいくつかのギャップがありました——カスタム@theme値がオートコンプリートされないことや、@utility定義が認識されないことがありました。最近のアップデートで大幅に改善されましたが、注目に値します。

3. エコシステムの成熟度。 v3周辺のエコシステムは巨大でした。Headless UI、Radix、shadcn/ui、Flowbite、DaisyUI——すべてがv3に対してテストされていました。v4サポートは展開中ですが、まだ普遍的ではありません。あるコンポーネントライブラリのv4互換性を修正するためにPRを送ったこともあります。

移行すべきか?#

v4と数週間暮らした後の判断フレームワークを紹介します:

今すぐ移行すべき場合:#

  • 新規プロジェクトを始める場合(明白な選択——v4で始めましょう)
  • プロジェクトにカスタムプラグインが最小限の場合
  • 大規模プロジェクトでパフォーマンス上の利点が欲しい場合
  • 既にモダンなTailwindパターン(スラッシュ不透明度、shrink-*など)を使っている場合
  • コンテナクエリが必要でプラグインを追加したくない場合

待つべき場合:#

  • v4をサポートしていないサードパーティTailwindプラグインに大きく依存している場合
  • 複雑なプログラマティックテーマ設定がある場合
  • プロジェクトが安定しており積極的に開発されていない場合(なぜ触るのか?)
  • 機能スプリントの真っ最中の場合(スプリント間で移行し、スプリント中にはしない)

移行すべきでない場合:#

  • v2以前を使っている場合(まずv3にアップグレードし、安定させてからv4を検討)
  • プロジェクトがあと数ヶ月で終了する場合(変更のコストに見合わない)

正直な感想#

新規プロジェクトにはv4が明白な選択です。CSSファースト設定はクリーンで、エンジンは劇的に高速で、新機能(コンテナクエリ、@starting-style、新バリアント)は本当に有用です。

既存プロジェクトには段階的なアプローチを推奨します:

  1. 今すぐ: 新規プロジェクトはすべてv4で始める
  2. 近いうちに: 小さな社内プロジェクトをv4に変換して実験する
  3. 準備ができたら: ビジュアルリグレッションテストを伴い、穏やかなスプリント中にプロダクションプロジェクトを移行する

準備すれば移行は苦痛ではありません。コードモッドが作業の80%を処理します。残りの20%は手動ですが、簡単です。中規模プロジェクトで1日、大規模プロジェクトで2〜3日を見込んでください。

Tailwind v4はTailwindがずっとあるべきだった姿です。JavaScript設定は当時のツーリングへの譲歩でした。CSSファースト設定、ネイティブカスケードレイヤー、Rustエンジン——これらはトレンドではなく、フレームワークがプラットフォームに追いついたということです。Webプラットフォームは進化し、Tailwind v4はそれに逆らうのではなく寄り添っています。

デザイントークンをCSSで書き、CSS機能でそれらを構成し、ブラウザ自身のカスケードに詳細度を任せる——それが正しい方向性です。ここに至るまで4つのメジャーバージョンが必要でしたが、その結果はこれまでで最も首尾一貫したTailwindのバージョンです。

次のプロジェクトで始めてください。後戻りはないでしょう。

関連記事