CSS-in-JSをやめて「CSS Modules + Tailwind」のいいとこ取りする

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


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

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

App Router(Server Components)と相性が悪い

Next.jsのApp Routerを本格的に導入してからぶつかった問題です。

ランタイム型のCSS-in-JSはサーバーコンポーネント(RSC)との相性があまり良くなく、スタイル周りのためにuse clientを付けるはめになったり、サーバーコンポーネントのメリットが活かしにくくなってました。

パフォーマンスへのランタイムオーバーヘッド

動きの激しい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側で管理するなら、使う意味なくなるのでは…という本末転倒な状態でした。

/* 悪い例 */
.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など)を考えると、ゼロランタイムの構成が一番しっくりきます。

同じくスタイリングで悩んでる人は試してみてください。