数年前まで Emotionやstyled-componentsを愛用していたのですが、最近はスタイリングを CSS Modules + Tailwind CSSのハイブリッドに移行しました。CSS-in-JSがしんどいと感じられている方の参考になれば幸いです。
CSS-in-JS から離れた理由
コンポーネントとスタイルが1ファイルになってるは最高と思っていたのですが、最近の開発環境の変化に伴って、いくつかの地味なストレスが溜まってきました。
1. App Router(Server Components)との相性の悪さ
Next.jsのApp Routerを本格的に導入してからの最大の壁です。
CSS-in-JSはランタイムでスタイルを生成するため、サーバーコンポーネント(RSC)では動作しません。結果、至る所にuse clientを記述する羽目になり、サーバーコンポーネントの利点が活かせなくなってしまいました。
2. パフォーマンスへのランタイムオーバーヘッド
動きの激しいUIやデータ量の多いテーブルコンポーネントを表示する際、Props変更のたびにスタイルが再計算されるため、画面がカクつく現象に遭遇しました。
プロファイルを取ってみると、CSS-in-JSのランタイム処理がボトルネックになっていることが判明。特に複雑なコンポーネントツリーでは顕著でした。
CSS Modules + Tailwind を選んだ理由
Tailwindだけでいい気もするかもしれませんが、実装してみると CSS ModulesとTailwind CSSの併用が最適解かなと落ちつきました。
理由は、両方の長所を活かしながら短所を補えるからです。
| 役割 | 役割分担 |
|---|---|
| Tailwind CSS | ユーティリティクラスで基本スタイル(余白、色、タイポグラフィなど)を高速に実装 |
| CSS Modules | 複雑なアニメーション、条件付きスタイル、クラス名が長くなりすぎる部分を整理 |
この組み合わせでランタイムのオーバーヘッドをゼロにしつつ、App Routerにも完全対応できます。
実装の雰囲気
移行後、コンポーネントは以下のような形になりました。
// Button.tsx
import styles from './Button.module.css';
type Props = {
variant?: 'primary' | 'secondary';
children: React.ReactNode;
};
export const Button = ({ variant = 'primary', children }: Props) => {
return (
<button
className={`${styles.btn} ${styles[variant]} text-white font-bold py-2 px-4 rounded-lg transition-all`}
>
{children}
</button>
);
};
/* Button.module.css */
.btn {
@apply shadow-md;
}
.primary {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
&:hover {
transform: translateY(-2px);
box-shadow: 0 8px 16px rgba(102, 126, 234, 0.4);
}
}
.secondary {
background: #4a5568;
&:hover {
background: #2d3748;
}
}
移行して良かったこと
表示速度の向上
HTMLに静的なクラス名が付くだけなので、ブラウザのレンダリングが軽くなりました。特に複雑なコンポーネントツリーで体感の差が出ます。
バンドルサイズの削減
CSS-in-JS のライブラリ分がJavaScriptバンドルから削れます。Tailwindも結局CSSなので、本来の用途(スタイルシート)に役割が戻った感じです。
Server Componentsとの親和性
use clientの宣言が必要なくなり、App Routerの恩恵(サーバー側での処理、データ取得の最適化など)をフルで活かせるようになりました。
開発体験の向上
クラス名を手で書く時間が減り、Tailwindの予測可能性と CSS Modulesのカスタマイズ性が両立できます。複雑なスタイルは CSSで、シンプルなものは Tailwindで、という使い分けが自然にできました。
移行中のつまづき
綺麗に見えますが、実際は失敗なこともしてました。
@applyの乱用
Tailwindのクラスが長くならないように、CSS Modules側に@applyを過剰に使ってて、これって普通にCSSを書くのと変わらないのでは…という本末転倒な状態でした。
/* 悪い例 */
.card {
@apply flex flex-col gap-4 p-6 bg-white rounded-lg shadow-md border border-gray-200;
}
.cardHeader {
@apply text-lg font-bold text-gray-900;
}
<div className={styles.card}>
<h2 className={styles.cardHeader}>タイトル</h2>
</div>
解決策
- 基本はTailwindのインラインクラスとして書く
- 3行を超えたり、グラデーションや複雑な疑似要素がいるときはCSS Modulesに切り替え
CSS Modulesが増えすぎるのを防ぎつつ、それぞれの役割分担ができました。
// Tailwindで書く
<div className="flex flex-col gap-4 p-4 bg-white rounded-lg shadow-md"></div>
/* 複雑な部分だけCSS Modulesで書く */
.cardHover {
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
transform: translateY(-4px);
box-shadow: 0 20px 25px rgba(0, 0, 0, 0.1);
}
}
学んだこと
技術トレンドは循環するといいますが、今回の「回帰」は退化ではなく、Web 標準(ゼロランタイム)の強みを活かしつつ、開発効率を保つための現実的な選択だと考えています。
CSS-in-JSのPropsを直接スタイルに渡せる圧倒的な手軽さが恋しくなる瞬間はありますが、Server Componentsなど現在のフロントエンドの潮流を考えると、この構成で十分満足しています。
コメント