CSS Modulesとは?Next.js・TypeScriptでの導入と型安全化の設定を解説
CSS Modulesは、CSSファイルに書いたクラス名をビルド時にファイル単位で一意な名前へ置き換え、グローバルな名前衝突をなくす仕組みです。書き方は素のCSSのままで、Next.jsとViteは追加パッケージなしで.module.cssを認識します。一方で、TypeScriptから型付きで扱おうとした途端に情報が散らばります。typescript-plugin-css-modulesはエディタ補完だけを担当し、ビルドを通すには別の型宣言が必要です。この記事では、公式ドキュメントと各パッケージの実バージョン(css-loader 7.1.4、typescript-plugin-css-modules 5.2.0、Next.js 16.2.12、Vite 8.2.0/2026年8月2日時点)をもとに、導入設定・型安全化の三択・つまずきやすい仕様変更を整理します。
まとめ
- CSS ModulesはICSS(Interoperable CSS)という中間形式にコンパイルされる仕様で、Next.jsもViteも
.module.cssを標準サポートしています。 - クラス名はcamelCaseが公式推奨です。kebab-caseは
styles.class-nameとドット記法で書けません。 - TypeScriptの型安全化は3択です。補完だけなら
typescript-plugin-css-modules、ビルドを通すにはワイルドカードの型宣言、クラス名の誤字までtscで落としたい場合のみtyped-css-modules。 - webpackを自前で組んでいる場合、css-loader 7でデフォルトインポートが壊れます(
esModuleが有効なときmodules.namedExportの既定値がtrueへ変更)。 - Next.jsでスタイルの当たり方が変わったときは、importの順序を疑います。CSSの適用順はコード上のimport順に従います。
- CSS Modulesは非推奨ではありません。仕様リポジトリの更新は2024年5月で止まっていますが、実装側は2026年も更新が続いています。
以下、仕組み、フレームワーク別の設定、型安全化、そしてTailwindやCSS-in-JSとの使い分けの順に見ていきます。
CSS Modulesの仕組みとローカルスコープの実体
ハッシュ付きクラス名とICSSへの変換
公式リポジトリ(css-modules/css-modules)の定義では、CSS Modulesとは「すべてのクラス名とアニメーション名がローカルスコープになるCSSファイル」であり、url()と@importはモジュールリクエスト形式(./xxxは相対、xxxはnode_modules内)として解決されます。実体はICSS(Interoperable CSS)という低レベルの中間形式へのコンパイルで、CSSそのものの言語仕様を拡張するものではありません。
JavaScript側からインポートすると、ローカル名からグローバル名へのマッピングオブジェクトが返ります。
/* Card.module.css */
.card {
border: 1px solid #ccc;
padding: 24px;
}
// Card.tsx
import styles from './Card.module.css';
export function Card() {
// styles.card は 'Card_card__1a2b3' のような一意な文字列
return <div className={styles.card} />;
}
クラス名の付け方について、公式のnaming.mdはcamelCaseを推奨しています。強制ではありませんが、kebab-caseにするとstyles.class-nameというドット記法が書けず、styles['class-name']のブラケット記法に切り替える必要があるためです。後述するcss-loaderの変換オプションを使わずに済ませたいなら、最初からcamelCaseで書くのが手数の少ない選択です。
composes・:global・@valueによるスタイル共有
CSS Modules固有の構文で実務上よく使うのはcomposesです。他のクラスを合成でき、合成元に付いた擬似クラス(:hoverなど)も一緒に引き継がれます。
/* button.module.css */
.base {
composes: reset from './shared.module.css';
composes: clearfix from global;
padding: 8px 16px;
}
公式のcomposition.mdは制約を明記しています。composesは他の宣言より前に書くこと、合成できるのは単一クラスセレクタのローカルスコープだけであること、複数のファイルから合成したときの適用順は未定義であること、そして循環依存を作らないことの4点です。後半2つが設計に効きます。別ファイルのクラス同士で同じプロパティに異なる値を持たせると、どちらが勝つかはビルド構成に依存します。循環した場合も上書き結果は未定義で、モジュールシステムがエラーを出す可能性があると公式は書いています。合成する側のクラスは単機能に保ち、値の上書きを期待しない設計にしてください。
定数を共有する@value構文もありますが、postcss-modules-valuesプラグインが前提です。色やサイズの共有が目的なら、CSSカスタムプロパティで足ります。ビルド設定を増やしてまで@valueを選ぶ理由は、テーマ値をJavaScript側へ:exportで渡す必要がある場合に限られます。
Next.js・Vite・webpackでの導入設定
Next.js App Routerでの.module.cssとSass併用
Next.js 16.2.12では設定ファイルへの追記は不要です。appディレクトリ配下に.module.cssを置き、コンポーネントからインポートするだけで動きます。
// app/blog/page.tsx
import styles from './blog.module.css';
export default function Page() {
return <main className={styles.blog} />;
}
SCSSで書きたい場合はsassパッケージを入れるだけで.module.scssと.module.sassが有効になります。公式のSassガイドでは、コンパイラ実装を切り替えるsassOptions.implementationも案内されており、sass-embeddedを指定できます。
npm install --save-dev sass
SCSSを足すかどうかは、mixinやループによるスタイル生成を実際に使うかで決めてください。ネストとカスタムプロパティはCSS側で標準化されており、変数とネストだけが目的ならビルド依存を1つ増やす見返りがありません。既存の.module.scss資産があるプロジェクトの移行先としては、拡張子を変えるだけで済む.module.cssが候補になります。
ここで押さえておきたいのは、Next.js公式が現在「大半のスタイリングにはTailwind CSSを使い、ユーティリティで足りないコンポーネント固有のスタイルにCSS Modulesを使う」という順序で推奨している点です。CSS Modulesを外したわけではなく、役割を限定した推奨に変わっています。
Viteのcss.modulesとLightning CSS使用時の分岐
Vite 8.2.0でも.module.cssは組み込みで解決されます。挙動を変えたい場合はcss.modulesで設定しますが、これはpostcss-modulesへそのまま渡されるオプションです。localsConventionの既定値はundefined、つまりクラス名は変換されません。camelCase化を有効にすると名前付きインポートも書けるようになります。
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
css: {
modules: {
localsConvention: 'camelCaseOnly',
generateScopedName: '[name]__[local]___[hash:base64:5]',
},
},
});
設定が丸ごと無視される事故の原因はほぼ1つです。css.transformerに'lightningcss'を指定している場合、css.modulesは効きません。Vite公式ドキュメントは「Lightning CSS使用時はcss.lightningcss.cssModulesを使うこと」と明記しています。css.modulesはPostCSS経路の設定だからです。css.transformerの既定値は'postcss'で、Lightning CSSへの切り替えは公式ドキュメント上Experimental扱いのままです。設定キーの分岐を承知したうえで切り替えてください。
Reactアプリでの扱いも同じです。Vite+Reactのテンプレートは.module.cssをそのまま解決するため、JSXではclassName={styles.card}と書くだけで済みます。CSS Modules自体はReactに依存した仕組みではなく、公式ドキュメントも「例はReactの構文だが、Reactに結び付いたものではない」と注記しています。Vue SFCやSvelteでも同じ.module.cssが使えます。
webpack単体構成でのcss-loader設定
フレームワークを使わずwebpackを組む場合は、css-loaderのmodulesオプションを有効にします。css-loader 7.1.4はwebpack 5.27.0以上(またはRspack)とNode.js 18.12.0以上が動作条件です。
// webpack.config.js
module.exports = {
module: {
rules: [
{
test: /\.module\.css$/i,
use: [
'style-loader',
{ loader: 'css-loader', options: { modules: true } },
],
},
],
},
};
この構成でバージョンを上げるときは、次のセクションの破壊的変更に必ず当たります。
css-loader 7でデフォルトインポートが壊れる破壊的変更
css-loader 7.0.0(2024年4月4日リリース)で、esModuleが有効なときmodules.namedExportの既定値がtrueへ変わりました。CHANGELOGに破壊的変更として記載されています。影響は明快で、これまで動いていたデフォルトインポートが機能しなくなります。
// css-loader 6 まで
import style from './style.css';
console.log(style.myClass);
// css-loader 7 以降(公式の移行ガイド)
import * as style from './style.css';
console.log(style.myClass);
コードを書き換えたくない場合は、公式が示す復帰設定で6系の挙動へ戻せます。
options: {
modules: {
namedExport: false,
exportLocalsConvention: 'as-is',
},
}
ここでexportLocalsConventionを明示しているのには理由があります。この既定値はnamedExportに連動し、trueならas-is、falseならcamel-case-onlyになるためです。namedExport: falseだけ書くと、ケバブケースのクラス名がcamelCaseへ変換されたうえ元の名前が消えるので、styles['my-class']が突然undefinedになります。もう1つ細かい仕様として、defaultという名前のクラスは予約語のため_defaultへ自動リネームされます(7.1.1、2024年4月10日)。
なお、この破壊的変更が影響するのは自前でcss-loaderを設定している構成だけです。Next.jsやViteの内蔵パイプラインを使っているなら、バージョン差異はフレームワーク側が吸収します。Turbopackとは?Next.js 16で既定になったRust製バンドラの設定と移行判断で扱っているとおり、Next.js 16のビルド経路自体が変わっている点も併せて確認してください。
TypeScriptで型安全に扱う3つの方法と使い分け
typescript-plugin-css-modulesによるエディタ補完
最新版は5.2.0(2025年7月23日)、TypeScript 4.0以上が対象です。導入はインストールとtsconfig.jsonへの追記だけで終わります。
npm install -D typescript-plugin-css-modules
{
"compilerOptions": {
"plugins": [
{
"name": "typescript-plugin-css-modules",
"options": { "classnameTransform": "camelCaseOnly" }
}
]
}
}
採用前に読んでおくべきなのは、READMEの「About this plugin」に書かれた制約です。これはTypeScript language service pluginであり、TypeScriptがコンパイル時のプラグインをサポートしていない(microsoft/TypeScript issue #16607)ため、次の2点はできないと明記されています。
- コンパイル時にエラーを出すこと
- プロジェクトにCSS Moduleのサポートを追加すること
つまりエディタ上ではstyles.cardが補完されても、tscやnext buildを通すための型解決は別に用意しなければなりません。ここを取り違えると「補完は効くのにビルドが落ちる」状態になります。Jestで.module.cssを解決したい場合も守備範囲外で、READMEはjest-css-modules-transform等を案内しています。
設定で触る機会が多いのはcustomMatcherとclassnameTransformです。customMatcherは既定が\.module\.((c|le|sa|sc)ss|styl)$なので、.m.cssのような独自拡張子を使うプロジェクトでは変更が必要になります。classnameTransformはcss-loaderのlocalsConventionと同じ振る舞いで、asIs(既定)、camelCase、camelCaseOnly、dashes、dashesOnlyから選択します。ビルド側の変換設定と揃えないと、補完に出るキーと実行時のキーがずれる点に注意してください。未知のクラス名への警告を止めたい場合は、既定falseのallowUnknownClassnamesをtrueにします。
declare moduleとallowArbitraryExtensionsによる型宣言
ビルドを通すための最短経路は、ワイルドカードのモジュール宣言です。プラグインのREADMEもこの形を例示しています。
// src/custom.d.ts
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
この方法はクラス名をstringで受けるだけなので、存在しないクラス名を書いてもコンパイルは通ります。誤字はプラグインの補完で防ぐ、という役割分担になります。
ファイル単位で型を書き分けたい場合は、TypeScript 5.0で追加されたallowArbitraryExtensionsを使います。app.cssに対してapp.d.css.tsという名前の宣言ファイルを置く形式です。
// app.d.css.ts
declare const css: {
cookieBanner: string;
};
export default css;
この形式を使うには、tsconfig.jsonのcompilerOptionsに"allowArbitraryExtensions": trueを追加します。TypeScript公式のリリースノートは、未知の拡張子のインポートは既定でエラーになり、バンドラ側で処理できている場合にこのオプションで抑止できると説明しています。
従来よく使われたapp.css.d.tsという命名は、Nodeのrequire解決規則に依存した書き方です。同リリースノートは、厳密にはこれがapp.css.jsに対する宣言ファイルと解釈されるため、moduleResolutionがnode16やnodenextのESMファイルではエラーになると説明しています。新しく書くなら.d.css.ts形式を選んでください。
typed-css-modulesでのd.ts生成と運用コスト
クラス名の誤字をtscで落としたい場合は、CSSから型定義を生成するtyped-css-modules(0.9.1、2024年1月31日、Node.js 18以上)を使います。
npx tcm src -w -a
生成された宣言ファイルにはクラス名がリテラルで並ぶため、存在しないキーを書くとビルドが落ちます。-a(--allowArbitraryExtensions)を付けると出力名が*.css.d.tsから*.d.css.tsへ変わり、前節のESM構成と揃います。TypeScript 5.0以降でmoduleResolutionにnode16やnodenextを使うなら、このフラグを付けてください。
代償は運用コストです。生成物をコミットするかCIで生成するかの判断、watchプロセスの常駐、CSS変更時の差分レビューが増えます。
3つの方法は規模で切り分けます。単一アプリならプラグイン+ワイルドカード宣言で十分で、生成物を持つ必要はありません。複数チームが参照する共通UIパッケージのように、クラス名の変更が他リポジトリへ波及する層だけtyped-css-modulesを入れます。この線引きなら、型生成の運用コストを払う場所が1か所に収まります。
| 方法 | エディタ補完 | tscで誤字検出 | ビルド通過 | 生成物の管理 |
|---|---|---|---|---|
| typescript-plugin-css-modules 5.2.0 | あり | 不可 | 不可 | 不要 |
| declare module のワイルドカード宣言 | なし | 不可 | 可 | 不要(d.ts 1本) |
| typed-css-modules 0.9.1 | あり | 可 | 可 | 必要(watch/CI) |
1行目と2行目は排他ではなく、併用が前提の組み合わせです。プラグインで補完を得つつ、ビルド用にワイルドカード宣言を1本置く構成が最も手数が少なくなります。
VS Codeで補完が効かないときの確認順序
プラグインを入れたのにstyles.で何も出ない場合、原因は次の順で切り分けます。
- ワークスペース版TypeScriptを使う設定になっているか。プラグインは
tsconfig.jsonから読み込まれるため、READMEもワークスペース版の使用を推奨としています。オプションを一切使わないならtypescript.tsserver.pluginPathsに指定する方法もあります。 customMatcherと実ファイルの拡張子が一致しているか。既定の正規表現は.module.を含むファイル名しか拾いません。- TypeScript Native Preview(
js/ts.experimental.useTsgo)を有効にしていないか。TypeScript 7系のネイティブ実装ではtsserverプラグインが読み込まれず、VS Code拡張には「TypeScript server plugins … will not be loaded because TypeScript Native Preview is enabled globally」という警告が2026年6月22日マージのPR #4063で追加されました。ただしこの警告は、VS Code拡張がcontributes.typescriptServerPluginsで提供するプラグインを走査した結果として表示されます。本記事の導入方法のようにtsconfig.jsonへ書いたプラグインは走査対象外で、警告なしに補完だけが消えます。typescript-go側のREADMEでもLanguage service(LSP)の状態は「in progress」のままなので、ネイティブ版を試すなら補完が落ちる前提で切り替えてください。 - ログを見る。コマンドパレットの
TypeScript: Open TS Server logでTSサーバーログを確認します。環境変数DISABLE_TS_PLUGIN_CSS_MODULESが設定されているとプラグインは早期に処理を打ち切るため、心当たりがあれば解除してIDEを再起動します。
Next.jsでスタイルの適用順が崩れる原因
CSS Modulesを使っていても、同じ要素に複数のクラスが当たる場面では優先順位の問題が残ります。Next.js公式ドキュメントは「CSSの順序はコード上でスタイルをインポートした順序に依存する」と明言しています。
// page.tsx
import { BaseButton } from './base-button';
import styles from './page.module.css';
export default function Page() {
return <BaseButton className={styles.primary} />;
}
この例ではBaseButtonのインポートが先にあるため、base-button.module.cssがpage.module.cssより前に並びます。importの並びを変えると、CSSの当たり方が変わるということです。
公式が挙げている対策のうち、事故を防ぐ効果が大きいのはESLintのsort-importsのようなimport自動並べ替えルールを切ることです。フォーマッタがimportを整列した瞬間にスタイルが崩れます。加えて、CSSのインポートは単一のエントリファイルへ寄せる、チャンク分割を制御するcssChunkingオプションを使う、といった選択肢があります。開発時と本番ビルドで順序が変わり得るため、最終確認はnext buildで行ってください。
グローバルCSSの扱いにも注意点があります。Next.jsはReactの標準スタイルシート機能を使ってSuspenseと統合している都合上、ルート遷移時にスタイルシートを取り外しません。グローバルCSSは本当に全ページ共通のものだけに絞り、コンポーネント固有のスタイルはCSS Modulesへ寄せるのが公式の推奨です。
Tailwind・CSS-in-JSとの使い分けと採用しない場面
まず「CSS Modulesは非推奨か」への回答です。非推奨ではありません。仕様リポジトリcss-modules/css-modulesは最終更新が2024年5月30日でアーカイブもされておらず、未解決のissueが119件、オープンなプルリクエストが5件残っています(スター17,966、2026年8月2日時点)。仕様側の動きは止まっている一方、実装側はcss-loaderが2026年2月16日に7.1.4をリリースし、Next.js 16とVite 8も標準サポートを維持しています。仕様が枯れていることと、使えなくなることは別です。
そのうえで、CSS Modulesを選ばないほうがよい条件を挙げます。
- propsに応じてスタイル値が動的に変わるUIが中心の場合。CSS Modulesは静的なクラス名の集合なので、状態の組み合わせが増えるほどクラス名が爆発します。CSSカスタムプロパティで値だけを流し込むか、CSS-in-JSへ寄せてください。
- デザイントークンを組織横断で共有する場合。トークンの一元管理はユーティリティクラス基盤の得意領域です。Tailwind CSS v4の主な変更点と新機能の概要で扱う設定方式の変更も判断材料になります。
- 画面が数枚しかない小規模なサイト。スコープ衝突が起きない規模でビルド設定を増やす意味はありません。素のCSSで十分です。
逆にCSS Modulesが有利なのは、コンポーネント単位のスコープが必要な中規模以上のアプリで、かつランタイムのスタイル生成を持ち込みたくない構成です。React Server Componentsのようにサーバー側でHTMLを組み立てる構成では、実行時コストがゼロであることがそのまま効きます。CSS-in-JSとの比較はstyled-componentsとは?メンテナンスモード後の現在地を、コンポーネント単位のレスポンシブ対応と組み合わせる話はCSS Container Queriesで実現するコンポーネント単位のレスポンシブ設計を参照してください。命名規則の逸脱を機械的に止めたい場合は、CSSリントツール最新バージョンStylelint v17の導入が合わせ技になります。
よくある質問
CSS Modulesとは何ですか?
クラス名とアニメーション名がファイル単位のローカルスコープになるCSSファイルの仕組みです。公式リポジトリの定義では、ICSS(Interoperable CSS)という低レベルの中間形式へコンパイルされます。書き方は通常のCSSと同じで、JavaScriptからインポートすると「元のクラス名→ビルド後の一意なクラス名」のマッピングオブジェクトが返ります。ブラウザの標準仕様ではなく、バンドラ側の実装によって成立する仕組みです。
CSS Modulesは非推奨ですか?
非推奨ではありません。仕様リポジトリの最終更新は2024年5月30日で止まっていますが、アーカイブはされていません。実装側はcss-loaderが2026年2月16日に7.1.4をリリースし、Next.js 16.2.12とVite 8.2.0はどちらも.module.cssを設定なしでサポートしています。ただしNext.js公式の推奨順序は「大半はTailwind CSS、ユーティリティで足りないコンポーネント固有のスタイルにCSS Modules」となっており、第一候補ではなくユーティリティで足りない部分の受け皿という位置づけです。
CSS Modulesのメリットは?
最大の利点は、クラス名の衝突を設計ルールではなくビルドで防げることです。BEMのような命名規約は人が守り続ける必要がありますが、CSS Modulesはファイルが違えば同じ.titleを書いても衝突しません。加えて、スタイルをコンポーネントと同じ場所へ置けるため依存関係が読み取りやすく、ランタイムのスタイル生成コストがゼロという特徴もあります。素のCSSの構文をそのまま使える点も、学習コストの面で効きます。
module.cssとmodule.scssの違いは何ですか?
スコープの仕組みは同じで、CSSの前処理があるかどうかだけが違います。.module.scssはSCSSの変数・ネスト・mixinを使えますが、Next.jsではsassパッケージのインストールが別途必要です。CSSのネストやカスタムプロパティが標準で使える現在、SCSSを足す判断はmixinやループ生成を実際に使うかどうかで決めてください。使わないなら.module.cssのままでビルド依存を1つ減らせます。
VS CodeでCSS Modulesの補完が効かないのはなぜですか?
多いのは、ワークスペース版のTypeScriptが使われていないケースです。typescript-plugin-css-modulesはtsconfig.json経由で読み込まれるため、VS Code同梱版のTypeScriptでは有効になりません。次に多いのがcustomMatcherと拡張子の不一致です。TypeScript Native Preview(js/ts.experimental.useTsgo)を有効にしている場合は、tsserverプラグイン自体が読み込まれず、拡張機能側が警告を表示します。