Tailwind CSSとEmotionの違い|v4での使い分けと共存
Tailwind CSSとEmotionは、どちらもReactのスタイリングで使われますが、スタイルを解決するタイミングが根本的に違います。Tailwind CSSはビルド時にCSSを生成し、EmotionはJavaScriptの実行中にCSSを組み立てます。この違いが、動的スタイルの書きやすさ、Server Componentsとの相性、そして両者を同居させたときの優先順位に直結します。2025年1月のTailwind CSS v4以降は設定方法とCSSの出力構造が変わり、両者をつなぐ定番だったtwin.macroも実質的に止まりました。ここでは2026年8月時点の一次情報にもとづき、比較・v4での前提・共存の実装パターンを整理します。
まとめ
- Tailwind CSSはビルド時解決、EmotionはランタイムでのCSS生成。動的な値をpropsやstateから算出するならEmotion、静的なレイアウトとデザイントークンの統一ならTailwind CSSが向きます。
- Tailwind CSS v4(最新は4.3.3、2026年7月16日公開)は
@tailwindディレクティブと自動読み込みのJS設定を廃止し、@import "tailwindcss";とCSS内の@themeに移行しました。 - v4は出力をネイティブのカスケードレイヤーに載せます。通常宣言どうしなら、レイヤー外で定義されるEmotionのクラスが記述順や詳細度に関係なくTailwindのユーティリティより優先されます。
- twin.macroは最新リリースが3.4.1(2024年1月19日)で、作者が2025年1月27日にv4対応の見通しが立たないと表明しています。v4環境では選択肢から外してください。
- v4以降の共存は、
@themeが生成するCSS変数をEmotion側からvar()で参照する方式が最も壊れにくい構成です。クラス名の分岐はclass-variance-authorityとtailwind-mergeが実質的な標準になっています。
Tailwind CSSとEmotionの設計思想の違い
Tailwind CSSはユーティリティファーストのCSSフレームワークです。bg-blue-500やtext-centerのように、あらかじめ定義されたクラスをマークアップに並べてスタイルを組み立てます。CSSはビルド時に生成され、使われたクラスだけが出力されます。ランタイムのコストはゼロで、HTMLを見ればスタイルが読めます。
EmotionはCSS-in-JSライブラリです。styledやcssでJavaScript側にスタイルを書き、実行時にハッシュ化したクラス名とスタイルシートを生成してDOMに挿入します。スタイルの値をJavaScriptの変数や条件式から組み立てられる点が最大の差で、テーマオブジェクトをContext経由で配る仕組みも備えています。
| 観点 | Tailwind CSS | Emotion |
|---|---|---|
| スタイル解決 | ビルド時 | ランタイム |
| 記述場所 | マークアップのclass属性 | JavaScript/TypeScript |
| 動的な値 | クラス名の切り替えで表現 | propsやstateから直接算出 |
| ランタイム負荷 | なし | あり(挿入とシリアライズ) |
| Server Components | そのまま利用可 | クライアント境界が必要 |
| CSSの出力先 | カスケードレイヤー内 | レイヤー外 |
ブラウザ要件の差も見落とされがちです。Tailwind CSS v4.0は@propertyとcolor-mix()に依存するため、公式ドキュメントがSafari 16.4以上、Chrome 111以上、Firefox 128以上向けの設計だと明記しています。Emotionの要件はReact 16.8以上だけです。それ以前のブラウザを保証する必要があるなら、v4ではなくv3.4系(LTSの3.4.19、2025年12月10日公開)に留まる判断になります。
Tailwind CSS v4で変わった導入と設定の前提
v4.0.0は2025年1月21日に公開されました。v3までの記事や社内手順書をそのまま流用すると、最初のインストール段階で行き詰まります。
インストールとCSSファイルの記述
Viteを使う場合、必要なパッケージは2つです。tailwind.config.jsを生成するコマンドは実行しません。
npm install tailwindcss @tailwindcss/vite
// vite.config.ts
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})
CSS側は@tailwind base;などの3行を書かず、1行のインポートに置き換わります。
@import "tailwindcss";
PostCSSを使う構成ではプラグインが@tailwindcss/postcssへ、CLIは@tailwindcss/cliへ分離しました。npx tailwindcss -i input.css -o output.cssはnpx @tailwindcss/cli -i input.css -o output.cssに変わります。
tailwind.config.jsから@themeへの移行
v4のデザイントークンはCSSの@themeブロックで定義します。JavaScriptの設定ファイルは後方互換として残っていますが、自動では読み込まれません。既存の設定を引き継ぐ場合は@configで明示的に指定します。
@import "tailwindcss";
@config "../../tailwind.config.js";
ただしcorePlugins、safelist、separatorはv4.0で非対応になりました。動的に組み立てたクラス名を残したい場合、公式ドキュメントはsafelistの代わりに@source inline()を使うよう案内しています。新規に書くなら次の形になります。
@import "tailwindcss";
@source inline("bg-red-500 bg-green-500");
@theme {
--color-brand-500: oklch(62.3% 0.214 259.8);
--font-display: "Satoshi", sans-serif;
--breakpoint-3xl: 120rem;
}
カスケードレイヤー導入による優先順位の変化
v4は出力をネイティブのカスケードレイヤーに載せます。配布パッケージのindex.cssは@layer theme, base, components, utilities;でレイヤー順を宣言し、ユーティリティを@layer utilitiesの中に閉じ込めています。
ここがEmotionと同居させたときの分岐点です。MDNのカスケードレイヤーの解説によれば、レイヤー内で定義されていないスタイルは、名前付きレイヤーや無名レイヤーで宣言されたスタイルを常に上書きします。Emotionが挿入するクラスはレイヤー外なので、読み込み順を調整しなくてもTailwindのユーティリティより強くなります。
例外は!importantです。カスケードレイヤーの優先順位は!importantが付くと逆転し、レイヤー内の重要宣言がレイヤー外の重要宣言を上回ります。Tailwind側で上書きしたいときは、v4の!修飾子を付けたbg-blue-600!のようなクラスを使ってください。これは@layer utilitiesの内側に!important付きで出力されるため、Emotionの通常宣言に勝ちます。
v3では両者とも同じ詳細度のクラスセレクタで、どちらが勝つかはスタイルシートの挿入順に依存していました。サーバーサイドレンダリングとクライアント側で挿入順がずれ、初回描画だけスタイルが違うといった不安定さの原因になっていた部分です。v4では結果が決定的になった代わりに、「Tailwindのユーティリティで後から上書きする」書き方は素直には成立しません。上書きの向きを設計時に決めておいてください。
Emotionの最新バージョンとReactでの記述
@emotion/reactは11.14.0(2024年12月9日)、@emotion/styledは11.14.1(2025年6月26日)が最新です。peerDependenciesはReact 16.8以上で、React 19でも利用できます。11.14.0でソースコードのTypeScript移行が完了し、型定義は手書きではなく実装から生成される形に変わりました。
styledとcssの基本形
import styled from '@emotion/styled'
const Button = styled.button`
background-color: var(--color-brand-500);
color: white;
padding: 8px 16px;
`
propsを受け取って値を算出できる点が、クラス名の付け替えでは書きにくい領域を埋めます。
const Bar = styled.div<{ ratio: number }>`
width: ${(props) => props.ratio * 100}%;
`
進捗バーの幅、チャートの座標、ユーザーが選んだ色のように、値が連続量で取りうる範囲が決まらないものはEmotionの領域です。逆に「3段階のサイズ」「5色のバリエーション」のように取りうる値が有限なら、Tailwindのクラス名を切り替えたほうがランタイムのコストを払わずに済みます。
App Routerでのクライアント境界の制約
EmotionはReact Server Componentsに対応していません。テーマ配布にReactのContextを使う設計のため、サーバー側で描画されるコンポーネントでThemeProviderやstyledを呼ぶと「createContext only works in Client Components」で失敗します。この件はemotion-jsリポジトリで2023年1月30日起票の議論(issue #2978)が未解決のまま続いており、React 19のスタイルホイスティングを使うRSC対応の提案(issue #3367、2026年3月9日)も検討段階です。
Next.jsのApp Routerで使うなら、Emotionを触るコンポーネントの先頭に"use client"を付けて境界を明示し、ThemeProviderはクライアント側のラッパーに閉じ込める構成が前提になります。ページ全体をクライアントコンポーネントにしてしまうとApp Routerを使う利点が消えるため、スタイルの動的部分だけを葉のコンポーネントに追い出す設計が必要です。この制約を許容できないなら、ビルド時に解決されるCSS Modulesの導入と型安全化を検討したほうが早いでしょう。
twin.macroがTailwind v4で止まった経緯と移行先
TailwindのクラスをEmotionのcssオブジェクトへ変換する統合ライブラリとして、長らくtwin.macroが使われてきました。v4環境では使えません。
事実関係は明快です。twin.macroの最新リリースは3.4.1で、公開日は2024年1月19日。peerDependenciesはtailwindcss: ">=3.3.1"のままです。v4対応を求めるissue #873(2024年11月29日起票)では、twin.macroが内部で参照しているtailwindcss/lib/util/toPathがv4のexportsに存在せず変換が失敗する、と具体的な破綻箇所が報告されました。作者は2025年1月27日の投稿で、v4対応を何度か試したがリライト後の内部関数にフックできず「良い解決策に到達できる気がしていない」と述べています。あわせて、CSS-in-JSから離れること自体で性能向上が見込めるため、いずれにせよTailwind本体への移行を勧める、との見解を示しました。issueは同日クローズされ、以降v4対応版はリリースされていません。
後継を名乗るライブラリも登場していません。tailwind-styled-componentsは最新が2.2.0(2022年9月6日)で週間ダウンロードは約3万件。ランタイムでTailwind相当のCSSを生成するTwindも、@twind/coreの最新が1.1.3(2023年1月24日)、週間ダウンロード約2.2万件で止まっています。「Tailwindのクラス名をCSS-in-JSの構文に変換する」という発想自体が、v4のビルド戦略と噛み合わなくなったと考えるのが実態に近いです。
移行先は、変換をやめて役割を分けることです。クラス名の組み立てはclass-variance-authority(0.7.1)、クラスの衝突解決はtailwind-merge(3.6.0、2026年5月10日公開)、条件結合はclsx(2.1.1)が担います。週間ダウンロードはclsxが約1億1,600万件、tailwind-mergeが約7,800万件、class-variance-authorityが約6,000万件です。同系統のtailwind-variants(3.3.1)もありますが、週間ダウンロードは約336万件と規模が一桁違います。shadcn/uiをはじめとする主要なコンポーネント配布形式が前提にしているのは前者の組み合わせで、実装の具体はshadcn/uiの導入手順とBase UI既定化の変更点で確認できます。
Tailwind v4とEmotionを共存させる実装パターン
デザイントークンをCSS変数で共有する構成
v4の@themeで定義した値は、コンパイル後に:rootのCSSカスタムプロパティとして出力されます。公式ドキュメントは「すべてのテーマ変数は、CSSをコンパイルすると通常のCSS変数になる」と説明しており、任意のCSSやインラインスタイルから参照できます。
つまり、Emotion側にテーマオブジェクトを二重管理する必要はありません。色やフォントの定義はTailwindの@themeに一本化し、Emotionからは変数名で引きます。
const Card = styled.section`
background: var(--color-brand-500);
border-radius: var(--radius-lg);
font-family: var(--font-display);
`
EmotionのThemeProviderでJavaScriptのテーマを配る方式に比べ、値の重複がなくなり、ダークモードもCSS変数の切り替えだけで両方に効きます。ThemeProviderはContextを使うためクライアント境界も要求しますが、CSS変数ならその制約もかかりません。
静的スタイルと動的スタイルの責務分割
レイアウト、余白、タイポグラフィ、ブレークポイントはTailwindのユーティリティに寄せます。これらはビルド時に確定し、ランタイムのコストがかかりません。Emotionに残すのは、実行時にしか決まらない値だけです。
<div className="flex items-center gap-4 rounded-lg p-4">
<Bar ratio={progress} />
</div>
この分割にはカスケードレイヤー上の理由もあります。同じプロパティを両方から指定すると、通常宣言どうしでは常にEmotionが勝ちます。同じプロパティを両者で書かない運用にしておけば、優先順位を意識せずに済みます。
Emotionのスタイルをレイヤーに載せる方法
どうしてもTailwind側を優先させたい場合は、Emotionのスタイル自体をレイヤーに入れられます。Emotionはテンプレートリテラル内のアットルールをそのまま出力するため、次のように書くと@layer emotion{.css-78sp14{color:red;}} の形で挿入されます。
const Card = css`
@layer emotion {
color: red;
}
`
あとはレイヤー順を先に宣言するだけです。レイヤーの優先順位は最初に現れた宣言で決まり、@layerのステートメントは@charsetと並んで@importより前に書ける数少ない記述なので、エントリのCSSを次の順で始めます。
@layer theme, base, components, emotion, utilities;
@import "tailwindcss";
これでEmotionのスタイルはユーティリティより下の層に置かれ、Tailwindのクラスが後勝ちになります。デザインシステムの土台をEmotionで書き、画面ごとの微調整をTailwindで当てる設計にはこの順序が合います。
バリアント管理のクラス名側への集約
twin.macroが担っていた「バリアントごとにスタイルを切り替える」役割は、class-variance-authorityで置き換えられます。
import { cva } from 'class-variance-authority'
import { twMerge } from 'tailwind-merge'
const button = cva('rounded-lg px-4 py-2', {
variants: {
intent: {
primary: 'bg-blue-600 text-white',
ghost: 'bg-transparent text-blue-600',
},
size: { sm: 'text-sm', lg: 'text-lg' },
},
defaultVariants: { intent: 'primary', size: 'sm' },
})
const className = twMerge(button({ intent: 'ghost' }), 'px-8')
呼び出し側から渡された追加クラスとの衝突はtwMergeが解決します。上の例ではpx-4とpx-8が競合し、後から渡したpx-8だけが残ります。バリアントの数が有限であるうちは、この方式ならランタイムでのシリアライズとstyle要素の挿入が発生せず、Server Componentsでもそのまま動きます。
どちらを選ぶかの判断基準
新規プロジェクトでApp Routerを採用するなら、Emotionを主役に据える構成は避けてください。クライアント境界の管理コストが継続的に発生し、RSC対応の見通しも立っていません。同じCSS-in-JSでも、メンテナンスモードに入ったstyled-componentsの現在地と合わせて、この系統全体の維持体制を確認したうえで判断すべき局面です。
逆に、Emotionを選ぶ理由が明確なケースもあります。既存のデザインシステムがJavaScriptのテーマオブジェクトを前提に組まれている場合、MUIなどEmotionを内部で使うUIライブラリに依存している場合、そしてスタイルの値がユーザー入力や計算結果に連動する場合です。管理画面のダッシュボードやエディタのように、幅や色をリアルタイムに算出する画面では、クラス名の列挙では表現しきれません。
Tailwind CSS単体で完結させる判断が向くのは、コンポーネントのバリエーションが列挙可能で、複数人が同じ規約でマークアップを書く必要があるプロジェクトです。React Nativeまで含めて共通化したいならTailwind CSSをReact Nativeで使うNativeWindという選択肢もあります。
既存のv3プロジェクトでtwin.macroを使っている場合、v4への移行はスタイリング方式の作り直しとセットになります。ブラウザ要件がv4の水準を満たしていないなら、3.4系のLTSに留まる判断も現実的です。期限を切って一気に移すより、新規コンポーネントから新方式で書き、既存部分を順次置き換えるほうが破綻しにくくなります。
よくある質問
Tailwind CSSとEmotionはどちらが速いですか。
描画時のコストはTailwind CSSのほうが小さくなります。Tailwindはビルド時にCSSを確定させるのに対し、Emotionは実行時にスタイルをシリアライズしてDOMへ挿入するためです。ただし体感差が出るのは、リストの各行でスタイルを生成するような繰り返し描画の場面に限られます。
Tailwind CSSとは何ですか。
ユーティリティファーストのCSSフレームワークです。既製のボタンやカードを提供するのではなく、余白や色といった単機能のクラスを組み合わせて見た目を作ります。この点がBootstrapのようなコンポーネント型フレームワークとの違いです。
Tailwind CSSの読み方は何ですか。
「テイルウィンドCSS」と読みます。tailwindは英語で追い風を意味する単語です。
Emotionのテーマ機能はTailwindのテーマと併用できますか。
併用は可能ですが、値を二重に持つと同期が崩れます。Tailwind v4の@themeはCSS変数を:rootに出力するため、Emotion側はvar(--color-...)で参照するだけで済みます。EmotionのThemeProviderは、既存コードがprops.themeを前提にしている場合の互換手段と位置づけるのが安全です。
ReactでEmotionを導入するには何をインストールしますか。
@emotion/reactと、styled構文を使うなら@emotion/styledを追加します。cssプロパティをJSXで使う場合はビルド設定でjsxImportSourceの指定が必要です。Next.jsではコンパイラ側のemotionオプションを有効にすると、Babelプラグインなしでソースマップと表示名が有効になります。
twin.macroはもう使えないのですか。
Tailwind CSS v3.x環境では動作します。v4環境では、twin.macroが参照する内部モジュールがv4に存在しないため変換が失敗します。作者自身が2025年1月にv4対応の見通しが立たないと表明し、その後もv4対応版はリリースされていません。v4を採用するなら、クラス名の組み立てはclass-variance-authorityとtailwind-mergeへ寄せてください。