CSS-in-JSをやめて、CSS Modules + Tailwindへ回帰した

数年前はEmotionやstyled-componentsを愛用してましたが、最近はスタイリングを CSS Modules + Tailwind CSSのハイブリッドで移行してみました。CSS-in-JSが最近しんどくなったと感じている方の参考になればと思います。


CSS-in-JS から離れた理由

コンポーネントとスタイルが一つにまとまってるのは便利だと思ってましたが、最近の開発環境の変化もあって、細かいところで地味なストレスが溜まっていきました。

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 shadow-md font-bold py-2 px-4 rounded-lg`}
    >
      {children}
    </button>
  );
};

/* Button.module.css */
.btn {
  transition: all 0.2s ease-in-out;
}

.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なので、本来の役割が戻った感じです。

RSCと相性がいい:use clientをいちいち書かなくていいので、App Routerの恩恵(サーバー側での処理、データ取得の最適化とか)をフルで活かせられます。

開発していて快適: クラス名で悩む時間が減って、直感的なTailwindとCSS Modulesの自由度がいいとこ取りできる感じです。


移行中のつまづき

綺麗に移行できたように見えますが、実際には失敗もありました。

@applyの乱用

Tailwindのクラスが長くならないように、CSS Modules側に@applyを使いまくってて、クラス名を考えてCSS側で管理するなら、結局Tailwindの意味ないのでは…という本末転倒な状態でした。

/* 悪い例 */
.btn {
  @apply text-white shadow-md font-bold py-2 px-4 rounded-lg;
}

<button className={styles.btn}>
  {children}
</button>

解決策

  • 基本はTailwindのインラインクラスとして書く
  • 3行を超えたり、グラデーションや複雑な疑似要素がいるときはCSS Modulesに切り替え

CSS Modulesが増えすぎるのを防ぎつつ、それぞれの役割分担ができました。


学んだこと

CSS Modules + Tailwind のハイブリッド構成は、ユーザーにも作る側にとってもバランスがいい組み合わせだと思いました。

Propsでそのままデザインを動かせるCSS-in-JSも良いですけど、今のトレンド(Server Componentsなど)を考えると、ゼロランタイムの構成が一番しっくりきます。

同じくスタイリングで悩んでる人は試してみる価値ありです。