UPSIDER Tech Blog

Next.js × MUI × SCSS環境で、差分検知しながらCSS ModulesをTailwindへ段階移行した話

こんにちは。

UPSIDERでフロントエンドを担当しているShuntaroです。

「このスタイル、どこで当たってるんだ?」と思ってDevToolsを開くと、SCSS、CSS Modules、MUIの追加CSSを行ったり来たり。そんな状態が続くと、変更の影響範囲を読むだけで時間が溶けます。

既存プロダクトは Next.js + MUI をベースに、SCSSと CSS Modulesが混在している構成です。今回はこれらを「すべて捨てて作り直す」話ではありません。 既存のMUI/SCSS資産は活かしつつ、記述コストの削減と将来的な構成の簡素化を目指して、CSS Modulesから Tailwind CSS へと段階的に軸足を移していくことにしました。

とはいえ、過渡期において異なるスタイリング手法を共存させることは、「意図せぬ崩れ」や「詳細度の衝突」といったリスクを伴います。

そこでこの記事では、Playwrightのスナップショットテストで差分を検知できる「安全網」を先に作り、その上で安全に Tailwind へ置き換えていった「移行フロー」と、その過程で直面した具体的な詰まりポイントをまとめます。

今回やったことは大きく2つです。

  • Playwright のスナップショットテストを導入して、UI差分を検知できる状態を先に作る

  • MUI / SCSS が残っている状況で Tailwind を共存させつつ、CSS Modulesからスタイルを段階的に置き換える

背景:なぜ段階移行が必要だったのか

既存のフロントエンドでは、次の要素が同時に存在していました。

  • CSS Modules がコンポーネントごとに増えている

  • MUIコンポーネントに対するsxや追加CSSが多い

  • グローバルSCSS(ユーティリティなクラス)が存在する

  • !important が散見される

この状態だとスタイルの入口が複数になり、意図しない差分が混ざりやすくなります。

そこで、既存の見た目を大きく崩さない前提で、Tailwindへ段階的に寄せていく方針にしました。

本記事は Next.js v14、Tailwind CSS v4、Playwright v1.50、MUI v5 を前提にしています。記載の設定は上記バージョンで動作確認しています。

まずやったこと:Playwright のスナップショットテスト導入

CSS ModulesをTailwind に置き換え始める前に、Playwrightのスナップショットテストを導入しました。

狙いは、置き換えの途中で入る意図しない見た目の差分を早めに拾えるようにすることです。特にSCSSで当たっているスタイルも差分として拾える状態を意識しました。

Tailwind導入で最初に起きたこと:導入しただけで差分が大量に出た

Tailwindを既存プロジェクトに入れた直後、スナップショットが大量に落ちました。

調査して分かった主な要因は次の通りです。

  • このプロジェクトでは既存のグローバルスタイルやMUI側のスタイルがすでに広い範囲に効いており、そこにTailwindを追加すると影響範囲が重なりやすい状態である

  • プロジェクトの既存SCSS側にTailwindと同名のクラスが存在する(p-, text-, flex- など)
    → 読み込み順や !important の影響で、見た目が予想より動くことがある

この状態だと、Tailwindを使い始める前にUIが動いてしまうので、導入方法を見直しました。

対応:移行初期は theme / utilities から入れる

移行の最初は、既存のMUI/SCSSへの影響を抑えるために、次の方針にしました。

  • いきなり全部を有効にせず、まずは themeutilities を読み込む

  • 既存SCSSの読み込み順を調整し、衝突しやすいユーティリティSCSSは後ろに寄せる

Tailwind の読み込みは Next.js のグローバルスタイル(pages/_app.tsx)で読み込んでいる global.scss に追加しました。

// Import Tailwind theme and utilities first to minimize unintended style changes during migration
@import "tailwindcss/theme";
@import "tailwindcss/utilities";

@import './override-mui.scss'; // important
@import '../override-notistack.scss';
@import '../override-uppy.scss';

@import './root-colors.scss'; // important
@import '../root-radius.scss';
@import '../root-elevation.scss';

@import './variables.scss'; // important
@import '../product-utilities.scss';
@import '../size.scss';
@import '../radius.scss';

// Import spacing, text, display, and position SCSS last to ensure existing classes take precedence over Tailwind
@import '../spacing.scss';
@import '../text.scss';
@import '../display.scss';
@import '../position.scss';

themeutilities だけを先に入れて、base styles は入れない形にしておくと、既存の MUI CssBaseline やグローバルSCSSの影響範囲を不用意に動かさずに、Tailwindのクラスを使い始められます。

その上で、既存のユーティリティSCSSは後ろで読み込むようにして、移行中に既存画面が崩れにくい順序にしています。

この状態で、既存の見た目を大きく動かさずに Tailwind のクラスを使い始められるようになりました。

CSS Modules → Tailwind の置き換え手順

  1. スナップショットテストが整っているページから、CSS ModulesをTailwindに置き換えました。基本の手順は次です。

  2. 対象ページのスナップショットを確認(必要なら追加)

  3. CSS ModulesのクラスをTailwind に置き換え

  4. スナップショット差分を確認

問題なければ反映して次に進む

この流れで置き換えを繰り返し、段階的に移行を進めました。

詰まったポイント1:スナップショットでは差分が出ないのに、staging環境で崩れた

ローカルのスナップショットでは差分が出ていないのに、staging環境に反映するとスタイル崩れが起きるケースがありました。

切り分けの結果、スナップショット取得時の条件(ビルド方法・実行環境)と、stagingで配信される成果物の条件が揃っていないと、ローカルでは再現しない見た目差分が出ることが分かりました。

スナップショットテスト自体は有効ですが、どの条件の成果物に対して撮るかまで含めて設計しないと取りこぼしが起きます。以降は、取得条件を揃える方針で進めています。

詰まったポイント2:Tailwind導入でMUI側の更新が必要になった

Tailwindの導入作業中にビルドエラーが発生し、最初はTailwind側の問題だと思って調査していました。

ただ、ログを追うと Tailwind の読み込み以前に依存解決で失敗しており、エラーの中心は MUI 周辺(@mui/*)でした(例:ERESOLVE could not resolve / Could not resolve dependency のような依存解決エラー)。

そこで、@mui/* のバージョンがプロジェクト内で揃っていない状態だったため、同一系列のパッチに揃える対応を入れました(例:@mui/material x.y.a → x.y.b。必要に応じて @mui/system@mui/icons-material も同様に調整)。結果としてビルドは通るようになりました。

Tailwindの導入に意識が向きすぎると、周辺依存の互換確認が後回しになりがちなので、以降はまずログで「どの段階で落ちているか(依存解決 / ビルド / 実行)」を切り分けてから当たりをつけるようにしています。

進め方:差分検知とレビューを前提に段階移行する

今回の取り組みは、置き換えを機械的に進めるのではなく、差分検知とレビューで品質を担保しながら段階的に進める、というやり方にしました。

スタイルは小さな差が積み上がると影響が広く出るので、テストで検知できる状態を先に作ってから置き換えを進めています。

現時点での成果

この取り組みはまだ途中ですが、現時点の状況は次の通りです。

  • スナップショットテスト:主要フローに導入

  • CSS Modulesの置き換え:対象ファイルの一部を完了

  • 体感できた変化:スタイルをどこに書くかで迷う場面が減った

今後の展望

次のステップとして考えているのは以下です。まずはCSS Modulesの削減を優先し、その後グローバルSCSSの整理に着手する予定です。

  • CSS Modules(コンポーネント単位SCSS)を段階的に置き換えて削除

  • グローバルSCSS(ユーティリティ類)も Tailwind theme に寄せ、デザイントークンを整理

  • スタイリングガイドを更新して、実装時の判断がブレにくい状態にする

  • 共通コンポーネントを利用できる画面・領域を増やし、段階的に適用範囲を広げる

まとめ

  • スタイルの入口が複数あると、差分が混ざりやすい

  • Tailwind移行を始める前に、PlaywrightのスナップショットでUI差分を検知できる状態を作っておくと進めやすい

  • 既存SCSSと同名クラスの衝突や読み込み順の影響で、導入だけでも見た目が動くことがある

  • スナップショットは取得条件がズレると取りこぼすので、成果物・実行条件を揃える設計が必要

  • 依存関係(MUIなど)の互換も含めて計画すると手戻りが減る

以上、Playwrightのスナップショット導入から始めたTailwind段階移行の記録でした。最後まで読んでいただきありがとうございました。

We Are Hiring !!

UPSIDERでは現在積極採用をしています。 ぜひお気軽にご応募ください。

herp.careers

herp.careers

UPSIDER Engineering Deckはこちら📣

speakerdeck.com