CSS Modulesは、CSSファイルに書いたクラス名をビルド時にファイルごとの一意な名前へ置き換え、名前の衝突をなくす仕組みです。書き方は普通のCSSのままで、Next.jsとViteは設定なしで.module.cssを読み込みます。つまずくのはTypeScriptと組み合わせる場面です。2026年7月8日にTypeScript 7.0.2がnpmのlatestになり、npm install typescriptで入る版はtsserverプラグインを読み込みません。エディタの言語サーバーもTypeScript 7にしていると、これまで定番だったtypescript-plugin-css-modulesの手順をそのまま踏んでも補完が出ません。この記事では、2026年11月1日時点の各パッケージ(Next.js 16.4.3、Vite 8.3.5、webpack 5.112.2、typescript-plugin-css-modules 5.3.0)と、tsc 7.0.2・webpack 5.112.2での実測をもとに、仕組みから型安全化までを整理します。
まとめ
- CSS Modulesはクラス名とアニメーション名をファイル単位のローカルスコープにする仕様で、バンドラがICSSという中間形式へ変換して実現します。ブラウザ標準ではありません。
- Next.js・Viteは
.module.cssを標準で扱います。webpackも5.109.0から本体が.module.cssを処理するようになり、css-loaderは2026年8月に非推奨になりました。webpackのネイティブCSSとcss-loader 7は、既定では名前付きエクスポートです。Next.js・Viteの標準的なデフォルトインポートとは区別してください。 - TypeScriptではビルドを通す型宣言(
declare module)が必須です。無いとtsc 7.0.2はTS2307で止まります。補完用のプラグインはビルドには効きません。 - TypeScript 7の言語サーバーはtsserverプラグインを読み込みません。補完やクラス名ジャンプが必要なら、エディタだけTypeScript 6.0で動かすか、誤字をtscで落とすd.ts生成へ寄せます。
- 名前が似たCSS Module Scripts(
import … with { type: 'css' })は別物で、2026年11月時点でSafari未対応のためBaselineに入っていません。 - CSS Modulesは非推奨ではありません。仕様リポジトリは2024年から止まっていますが、webpackが本体に取り込み、Next.js・Viteも標準で対応しています。
以下、仕組み、メリットと弱点、フレームワーク別の設定、TypeScriptでの型安全化、TypeScript 7での回避策、似た名前の別仕様との違い、Tailwind CSSや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;
}
.title {
font-weight: bold;
}
// Card.tsx
import styles from './Card.module.css';
export function Card() {
// styles.card は 'Card_card__1a2b3' のような一意な文字列になる
return (
<div className={styles.card}>
<h2 className={styles.title}>見出し</h2>
</div>
);
}
別のファイルで同じ.titleを書いても、出力されるクラス名はファイルごとに違うので衝突しません。クラス名の付け方は、公式のnaming.mdがcamelCaseを推奨しています。kebab-caseで.card-titleと書くと、styles.card-titleというドット記法が使えずstyles['card-title']と書く必要があるためです。後から変換オプションで吸収するより、最初からcamelCaseで書くほうが手数は少なく済みます。
composes・:global・@valueの使いどころ
CSS Modules固有の構文で実務によく出るのはcomposesです。ほかのクラスのスタイルを合成でき、合成元の:hoverなどの擬似クラスも引き継がれます。グローバルなクラスを使いたいときは:global(.className)で囲みます。
/* button.module.css */
.base {
composes: reset from './shared.module.css';
composes: clearfix from global;
padding: 8px 16px;
}
:global(.is-dark) .base {
color: #fff;
}
公式のcomposition.mdには制約が4つあります。composesはほかの宣言より前に書くこと、合成できるのは単一クラスのローカルセレクタだけであること、複数ファイルから合成したときの適用順は未定義であること、循環させないことです。設計に効くのは後半の2つで、別ファイルのクラス同士で同じプロパティに違う値を持たせると、どちらが勝つかはビルド構成次第になります。合成元のクラスは単機能に保ち、値の上書きを期待しない書き方にしてください。
定数を共有する@valueの対応は実装によります。PostCSSではpostcss-modules-valuesが処理しますが、webpackのネイティブCSSは追加プラグインなしで対応しています。色や余白の共有ならCSSカスタムプロパティで足ります。@valueや:exportが活きるのは、テーマの値をJavaScript側へ渡す場面と、ブレークポイントのようなメディアクエリの条件をファイル間で共有する場面です。カスタムプロパティはメディアクエリの条件に使えないため、後者は@valueの出番になります。
CSS Modulesのメリットと弱点
クラス名の衝突をビルドで防げるメリット
最大の利点は、衝突の防止を人の注意ではなくビルドに任せられることです。BEMのような命名規約はチーム全員が守り続けて初めて機能しますが、CSS Modulesはファイルが違えば同じ名前を書いても衝突しません。スタイルの依存関係もimport文に現れるので、どのコンポーネントがどのCSSを使っているかをコードから追えます。
実行時にスタイルを生成する処理がない点も大きな違いです。CSS-in-JSの多くはブラウザ上でスタイルのルールを組み立てますが、CSS Modulesはビルド時に通常のCSSファイルとクラス名の文字列へ変換されるだけです。React Server Componentsのようにサーバー側でHTMLを組み立てる構成でも、追加の仕組みなしで動きます。書くのは普通のCSSなので、ネストやカスタムプロパティといった標準の機能がそのまま使えます。
動的スタイルと複数クラスの合成に弱い点
弱点は3つの場面に集中します。1つ目は、propsに応じて値が細かく変わるスタイルです。CSS Modulesのクラスは静的な集合なので、状態の組み合わせが増えるほどクラスが増えます。値だけが変わるならCSSカスタムプロパティをstyle属性で渡し、クラスは増やさないのが定石です。
// 値だけを変える場合はクラスを増やさずカスタムプロパティで渡す
<div
className={styles.bar}
style={{ '--progress': `${percent}%` } as React.CSSProperties}
/>
2つ目は、前述のcomposesの適用順が複数ファイルで未定義になる点です。3つ目は、ビルド後のクラス名が読みにくくなる点で、ブラウザの開発者ツールでCard_card__1a2b3のような名前を追うことになります。ViteのPostCSS構成では、開発時だけcss.modules.generateScopedNameで[name]__[local]を含む形式にしておくと、どのファイルのどのクラスかが名前から分かります。
Next.js・Vite・webpackでのCSS Modules導入設定
Next.js App Routerでの.module.cssと.module.scssの導入
Next.js 16.4.3では設定ファイルへの追記は要りません。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が使えるようになります。Next.jsのSassガイドは、コンパイラを切り替えるsassOptions.implementationでsass-embeddedを指定する方法も案内しています。
npm install --save-dev sass
SCSSを足すかどうかは、mixinやループでスタイルを生成するかで決めてください。ネストとカスタムプロパティはCSS自体で使えるので、変数とネストのためだけにビルド依存を1つ増やす理由はありません。
Next.jsの公式ドキュメントは現在、大半のスタイルはTailwind CSSで書き、ユーティリティで足りないコンポーネント固有のスタイルにCSS Modulesを使うという順序で案内しています。CSS Modulesを外したわけではなく、役割を絞った推奨です。
Viteのcss.modules設定とLightning CSS使用時の設定先
Vite 8.3.5でも.module.cssは組み込みで解決されます。挙動を変えるときはcss.modulesを使います。このオプションはpostcss-modulesへそのまま渡され、localsConventionの既定値はundefined(クラス名を変換しない)です。camelCase化を有効にすると、.card-titleをstyles.cardTitleで参照できます。
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({
css: {
modules: {
localsConvention: 'camelCaseOnly',
generateScopedName: '[name]__[local]___[hash:base64:5]',
},
},
});
Lightning CSSへ切り替えている場合は、CSS Modulesの設定先を確認してください。css.transformerに'lightningcss'を指定しているとcss.modulesは効きません。Viteの設定リファレンスは、Lightning CSSを使うときはcss.lightningcss.cssModulesで設定するよう明記しています。css.transformerの既定値は'postcss'で、Lightning CSSへの切り替えはExperimental扱いです。Vite本体の導入手順はVite + Reactの環境構築手順|Viteとは・Vite 8.3の変更点とTypeScript 7対応で扱っています。
webpack 5.109以降のネイティブCSSでの.module.css対応
webpackを自前で組む場合の前提は、2026年に大きく変わりました。webpack 5.109.0(2026年7月23日)からexperiments.cssの既定値が'auto'になり、.cssにローダーを割り当てていなければ本体がCSSを直接処理します。webpackのNative CSSガイドによれば、/\.css$/iに付く既定のモジュールタイプはcss/autoで、*.module.cssと*.modules.cssだけがCSS Modulesとして扱われます。
webpack 5.112.2で、CSS用のルールを1つも書かずに次のコードをビルドすると、import * asは動き、デフォルトインポートの行はビルドエラーになりました。
// src/index.mjs
import * as styles from './Card.module.css'; // OK
import def from './Card.module.css'; // NG
// ビルド結果
// ERROR in ./src/index.mjs 3:66-69
// export 'default' (imported as 'def') was not found in './Card.module.css' (possible exports: card, card-title)
ネイティブCSSはパーサーのnamedExportsが既定でtrueのためです。既存コードのデフォルトインポートを残すなら、module.parser['css/auto'].namedExportsをfalseにします。この設定でビルドするとdef.cardもdef['card-title']も値を返し、kebab-caseの名前はそのまま残りました。名前の変換はmodule.generator['css/auto'].exportsConvention(既定'as-is')で別に指定します。
// webpack.config.js
module.exports = {
module: {
parser: {
'css/auto': { namedExports: false },
},
},
};
ただし、ガイド自身がexperiments.cssはまだexperimentalで、webpack 6で既定になる見込みだと書いています。本番の設定を移すのは、ビルド結果のCSSを差分で確認できる体制があるときにしてください。
css-loader 7の非推奨化とデフォルトインポートの戻し方
これまでwebpackでCSS Modulesを担っていたcss-loaderは、7.1.5(2026年8月28日)を最後に非推奨となり、リポジトリもアーカイブされました。READMEは、webpackがCSSを自分で解析するようになったためcss-loaderは不要になったと説明しています。既存の設定はそのまま動きます。.cssにローダーを割り当てたルールがあればネイティブCSSは無効のままなので、ルール単位で移行できます。移行用にはnpx codemod @webpack/css-plugins-to-native-cssが用意されています。
css-loaderを使い続ける構成では、7.0.0(2024年4月4日)の破壊的変更に注意が要ります。esModuleが有効なときmodules.namedExportの既定値がtrueに変わり、CHANGELOGの移行例どおりimport style fromをimport * as style fromへ書き換える必要があります。コードを書き換えずに6系の挙動へ戻すなら、次の2つを両方指定します。
{
loader: 'css-loader',
options: {
modules: {
namedExport: false,
exportLocalsConvention: 'as-is',
},
},
}
exportLocalsConventionまで書くのは、css-loaderではこの既定値がnamedExportに連動し、trueならas-is、falseならcamel-case-onlyになるためです。namedExport: falseだけを書くとkebab-caseのクラス名がcamelCaseに変換され、元の名前が消えます。styles['my-class']が突然undefinedになるのはこのためで、前節のネイティブCSSとは挙動が違います。Next.jsやViteの内蔵パイプラインを使っているなら、こうした差はフレームワーク側が吸収します。Next.js 16のビルド経路の変化はTurbopackとは:Next.js 16で既定になったRust製バンドラの設定と移行判断で確認できます。
TypeScriptでのCSS Modulesの型宣言・補完・誤字検出
CSS ModulesとTypeScriptの関係は、TypeScriptが.cssファイルの中身を読めないという一点に尽きます。インポート自体を通す型宣言、エディタでの補完、存在しないクラス名の検出は、それぞれ別の道具が担います。
ビルドを通す:declare moduleとallowArbitraryExtensions
型宣言を何も置かずに.module.cssをインポートすると、tsc 7.0.2は次のエラーで止まります。
src/Card.tsx(1,20): error TS2307: Cannot find module './Card.module.css' or its corresponding type declarations.
最短の解決は、ワイルドカードのモジュール宣言を1ファイル置くことです。Viteはvite/clientの型(テンプレートのtsconfigで"types": ["vite/client"])に、Next.jsはnext-env.d.ts経由で読み込む型に同等の宣言を持っているので、テンプレートから作ったプロジェクトでは不要です。自前で組んだ構成では次の1ファイルが必要になります。
// src/css-modules.d.ts
declare module '*.module.css' {
const classes: { [key: string]: string };
export default classes;
}
この宣言はクラス名をstringで受けるだけなので、styles.carddのような誤字もコンパイルを通ります。逆にnoUncheckedIndexedAccessを有効にしているプロジェクトでは、すべてのクラスがstring | undefinedになります。classNameのようにundefinedを受け付ける場所へはそのまま渡せますが、string型の変数への代入ではTS2322、string型の仮引数への受け渡しではTS2345が出ます(tsc 7.0.2で確認)。
ファイルごとに型を書き分けたい場合は、TypeScript 5.0で入ったallowArbitraryExtensionsを使います。app.cssに対してapp.d.css.tsという名前の宣言ファイルを置く形式で、tsconfig.jsonに"allowArbitraryExtensions": trueを足します。従来のapp.css.d.tsという命名は、TypeScript 5.0のリリースノートによればapp.css.jsの宣言と解釈されるため、moduleResolutionがnode16やnodenextのESMファイルではエラーになります。新しく書くなら.d.css.ts形式を選んでください。
エディタで補完する:typescript-plugin-css-modules
typescript-plugin-css-modulesの最新は5.3.0(2026年10月24日)です。この版でオプトインのキャッシュが入り、enginesにNode.js 20以上が明記されました。peerはtypescript >=4.0.0のままです。導入はインストールとtsconfig.jsonへの追記で終わります。
npm install -D typescript-plugin-css-modules
{
"compilerOptions": {
"plugins": [
{
"name": "typescript-plugin-css-modules",
"options": { "classnameTransform": "camelCaseOnly" }
}
]
}
}
READMEは、これがTypeScriptのlanguage service pluginであり、TypeScriptがコンパイル時のプラグインをサポートしていない(microsoft/TypeScript #16607)ため、コンパイル時にエラーを出すことも、プロジェクトにCSS Modulesのサポートを追加することもできないと明記しています。エディタでstyles.cardが補完されても、tscやnext buildを通すには前節の型宣言が別に要ります。「補完は効くのにビルドが落ちる」はこの取り違えです。
classnameTransform(既定asIs)はビルド側のlocalsConventionと揃えてください。揃えないと、補完に出るキーと実行時のキーがずれます。
誤字をtscで落とす:typed-css-modulesとcss-modules-kit
存在しないクラス名をCIで落としたいなら、CSSから型定義を生成します。typed-css-modules 0.9.1(2024年1月31日)は-aを付けると.d.css.ts形式で出力します。ビルド側でlocalsConvention: 'camelCaseOnly'のようにクラス名を変換しているなら、型の生成にも-c(--camelCase)を付けて揃えます。揃えないと型にはcard-titleが残り、styles.cardTitleが型エラーになります。
npm install -D typed-css-modules
# 開発中は変更を監視して生成
npx tcm src -a -c -w
# CIでは-wを付けずに1回だけ生成してからtscを実行
npx tcm src -a -c && npx tsc --noEmit
Card.module.cssからはCard.module.d.css.tsが生成され、クラス名が個別のプロパティ名として並び、各プロパティの値はstring型になります。allowArbitraryExtensionsを有効にしてtsc 7.0.2を通すと、styles.carddは次のように落ちます。
src/Card.tsx(3,33): error TS2551: Property 'cardd' does not exist on type '{ readonly card: string; readonly cardTitle: string; }'. Did you mean 'card'?
もう1つの選択肢がcss-modules-kit(1.4.0、2026年7月9日)です。tsserverプラグインとd.ts生成(cmkコマンド)、stylelint・ESLintのプラグインをまとめたツール群で、エディタではstyles.buttonからCSS側の定義へのジャンプ、CSSとTSXをまたいだリネーム、参照の検索が使えます。生成したd.tsは既定でgeneratedディレクトリに出るため、tsconfig.jsonに"rootDirs": [".", "generated"]と"cmkOptions": { "enabled": true }を足します。SassとLessには対応していないので、.module.scssが中心のプロジェクトでは使えません。
TypeScript 7との相性には注意が要ります。作者はissue #377で、codegen・ts-plugin・linter-pluginの3つとも現時点ではTypeScript 7以降で動かないと明記しています。実際に@css-modules-kit/codegen 1.4.0のpeerはtypescript ^5.7.3 || ^6.0.0で、TypeScript 7.0.2と同じプロジェクトに入れるとnpm installがERESOLVEで止まりました。一方、TypeScript 6.0.3の環境で生成したd.ts自体は普通の型定義なので、tsc 6.0.3でもtsc 7.0.2でも誤字をTS2551で検出しました。
| 方法 | ビルド通過 | tscで誤字検出 | エディタ補完 | TS7単独で動くか |
|---|---|---|---|---|
| declare module(ワイルドカード) | 可 | 不可 | 型はstringのみ | 動く |
| typescript-plugin-css-modules 5.3.0 | 不可 | 不可 | あり | 読み込まれない |
| typed-css-modules 0.9.1 | 可 | 可 | 生成後は型で補完 | 動く |
| css-modules-kit 1.4.0 | 可 | 可 | あり(定義ジャンプ・リネーム) | 動かない |
使い分けは、CIで何を検査したいかで決めます。クラス名の誤字はレビューと画面確認で拾えれば十分なら、ワイルドカード宣言だけで足ります。補完が欲しい人はプラグインを足せば済みます。誤字やクラス名の削除をCIで機械的に落としたいならd.ts生成を入れます。共通UIパッケージのように、クラス名の変更が他のリポジトリへ波及する層では特に効きます。代わりに生成物の管理という運用が増えるので、導入前にCIへ組み込む手順まで決めておいてください。
TypeScript 7でCSS Modulesのプラグインが効かない問題
TypeScript 7がtsserverプラグインを読み込まない理由
TypeScript 7はGoで書き直されたネイティブ実装で、エディタとの連携もLSPで行います。従来のプラグインはNode.jsで動くtsserverのプロセスに読み込まれていましたが、TypeScriptチームはmicrosoft/TypeScript #63800で「Go実装ではサードパーティのコードをサーバープロセスに動的リンクできない」と説明しています。実際、npm install [email protected]で入るパッケージのlibにはtsc.jsなどしかなく、tsserver.jsがありません(2026年11月1日に確認)。
TypeScript 7.0の公式発表(2026年7月8日)も、TypeScript 7はlanguage server pluginが要らない場面で使うよう推奨しており、Vue・MDX・Astro・Svelteなどのプロジェクトは当面TypeScript 6.0を使い続ける必要があると書いています。typescript-plugin-css-modulesもcss-modules-kitのts-pluginも、この「language server plugin」に当たります。TypeScript 7用のVS Code拡張は、ほかの拡張機能が提供するtsserverプラグインについては「will not be loaded because TypeScript 7 is enabled globally」という警告を出しますが、tsconfig.jsonのpluginsに書いたプラグインは警告の対象外です。tscもエラーを出さないので、何が起きたか分からないまま補完だけが消えます。
影響は新規プロジェクトにも及びます。create-vite 9.3.3のreact-tsテンプレートは"typescript": "~7.0.2"を指定しているので、2026年11月時点でnpm create vite@latestから作ったプロジェクトにプラグインを足しても、エディタがTypeScript 7の言語サーバーで動いている限り補完は出ません。
エディタだけTypeScript 6.0で動かす回避策
手早いのはVS Code側で戻す方法です。公式発表によれば、コマンドパレットで「Disable TypeScript 7 Language Server」を実行するとTypeScript 6.0の言語サーバーに戻ります。
プロジェクト単位で固定するなら、typescriptパッケージを6.0系に戻します。tscの型チェックだけTypeScript 7で速く回したい場合は、7.0系を別名で足します。
npm install -D typescript@~6.0.3 ts7@npm:[email protected]
この構成ではnode_modules/typescriptが6.0.3になり、lib/tsserver.jsがあるのでVS Codeの「Use Workspace Version」とtsserverプラグインが従来どおり使えます。tscコマンドはどちらにも存在し、手元の検証ではnode_modules/.bin/tscが7.0.2側にリンクされました。CIではnode node_modules/ts7/bin/tsc --noEmitのようにパスで指定すると取り違えません。なお、6.0系を別名で入れるための@typescript/typescript6(6.0.2)にはtsserver.jsが含まれていないため、エディタ用には使えません。TypeScript 7への移行全体はTypeScript 7(TS7)とは?Go製tscで約10倍速く|7.0.2への移行とtsgo・7.1 betaの現状で扱っています。
TS7単独で完結させたいなら、typed-css-modulesでd.tsを生成します。型がファイルとして存在するので、プラグインなしでも補完と誤字検出が効きます。CSS側の定義へジャンプしたいならTS6併用、CIで誤字を落とせれば十分ならd.ts生成と、目的で選んでください。
VS Codeで補完が効かないときの確認順序
プラグインを入れたのにstyles.の後に候補が出ない場合は、次の順に切り分けます。
- エディタの言語サーバーがTypeScript 7になっていないか。TypeScriptのファイルを開いた状態でステータスバーの言語表示(
{})を開くと、動いている版が表示されます。7系なら前節の回避策へ進みます。npx tsc -vはCLIの版で、エディタの版とは別なので判定に使えません。 - VS Codeがワークスペース版のTypeScriptを使っているか。プラグインは
tsconfig.json経由で読み込まれるため、READMEもワークスペース版の使用を推奨しています。 customMatcherと実際の拡張子が一致しているか。既定の正規表現は.module.を含むファイル名しか拾いません。- TSサーバーのログを見る。コマンドパレットの「TypeScript: Open TS Server log」で確認します。環境変数
DISABLE_TS_PLUGIN_CSS_MODULESが設定されているとプラグインは処理を打ち切るので、心当たりがあれば外してエディタを再起動します。
1つ目を飛ばすと、残り3つをいくら確かめても直りません。2026年7月以降にtypescriptを入れ直したプロジェクトでは、まずエディタ側の版を見てください。
Next.jsでスタイルの適用順が崩れる原因
CSS Modulesでクラス名の衝突は防げても、同じ要素に複数のクラスが当たると優先順位の問題は残ります。Next.jsのCSSガイドは、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の並びを変えると、同じ詳細度のスタイルはどちらが勝つかが入れ替わります。
公式が挙げる対策のうち、事故を防ぐ効果が大きいのはESLintのsort-importsのような、importを自動で並べ替えるルールを切ることです。フォーマッタがimportを整列した瞬間にスタイルが崩れます。ほかに、CSSのインポートを1つのエントリファイルへ寄せる、共通のスタイルは共有コンポーネントへ切り出して重複したインポートを減らすといった方法もあります。チャンク分割はexperimental.cssChunkingで制御でき、既定値のtrueはimport順から依存関係を判断してCSSファイルを結合します。'strict'にするとwebpackでimport順をそのまま守りますが、チャンクとリクエストの数が増えることがあります。開発時と本番ビルドで順序が変わることがあるため、最終確認はnext buildの結果で行ってください。
グローバルCSSにも注意点があります。Next.jsはReactのスタイルシート機能を使ってSuspenseと統合しているため、ページを移動してもスタイルシートを取り外しません。グローバルCSSは全ページ共通のものだけに絞り、コンポーネント固有のスタイルはCSS Modulesへ寄せるのが公式の推奨です。
CSS Module Scripts・@scope・Sassモジュールとの違い
「CSS モジュール」と呼ばれるものは複数あり、検索結果や生成AIの回答でも混同が起きやすい分野です。ここで扱うCSS Modulesはバンドラが実装する仕組みで、以下の3つとは別物です。
CSS Module ScriptsのCSSStyleSheet読み込みと対応ブラウザ
CSS Module Scriptsは、HTML仕様で定められたブラウザ標準の機能です。import sheet from './style.css' with { type: 'css' };と書くと、CSSファイルがCSSStyleSheetオブジェクトとして読み込まれます。クラス名の書き換えは行われず、得られたシートをdocument.adoptedStyleSheetsやShadow DOMのshadowRoot.adoptedStyleSheetsに追加して使います。
import sheet from './card.css' with { type: 'css' };
const host = document.querySelector('#card');
const root = host.attachShadow({ mode: 'open' });
root.adoptedStyleSheets = [sheet];
ブラウザの対応は、web-features 3.44.0のデータでChrome・Edge 123以降とFirefox 147以降です。Safariは、WebKitに2026年10月6日に実装が取り込まれたものの、リリース済みの版はまだ対応していないため、2026年11月時点でBaselineには入っていません。スコープを作る仕組みとしてはShadow DOMと組み合わせる前提なので、Web Componentsでの使い方はShadow DOMとは?open/closedの違いとslot・イベント・宣言型の実装例【2026年】を参照してください。
@scopeの適用範囲とBaselineの状況
@scopeは、セレクタが効く範囲をDOMの部分木に限定するCSSの標準機能です。クラス名を書き換えずにスコープを作れるので、CSS Modulesの代わりとして語られることがあります。web-featuresのデータでは、Chrome・Edge 143、Firefox 146、Safari 26.4がそろった2026年3月24日にBaseline(Newly available)になりました。
@scope (.card) to (.card-content) {
img {
border-radius: 8px;
}
}
ただし@scopeが防ぐのは「スタイルが範囲外へ漏れること」で、クラス名そのものの衝突は防ぎません。別のチームが同じ.cardを別の意味で使えば、スコープの起点自体がぶつかります。Newly availableの段階では古いブラウザの利用者も残るので、2026年時点で大規模なアプリのスコープ管理を@scopeだけに任せるのは早いと考えます。
Sassの@useと.module.scssの役割の違い
「SCSSのmodules」で検索すると、Sassの@use・@forwardによるモジュールシステムの説明が出てきます。これはSassの変数・mixin・関数をファイル単位で名前空間に分ける仕組みで、出力されるクラス名には何もしません。
一方の.module.scssは、SCSSをCSSにコンパイルしたあとでCSS Modulesのクラス名変換をかけるファイルです。1つのファイルで両方を使うこともでき、.module.scssの中で@use 'variables' as v;と書けば、Sassの名前空間とCSS Modulesのスコープが別々に働きます。Sassの@importはDart Sass 1.80.0で非推奨になり、Dart Sass 3.0.0で削除される予定です。.module.scssで共通の変数を読み込んでいるなら、@useへ移しておくと後で困りません。
Tailwind・CSS-in-JSとの使い分けと非推奨かどうか
まず「CSS Modulesは非推奨か」への答えは、非推奨ではありません。仕様リポジトリcss-modules/css-modulesは最後のコミットが2024年3月23日で、未解決のissueが119件、オープンなプルリクエストが5件残っています(スター18,040、アーカイブはされていない/2026年11月1日時点)。仕様は止まっていますが、実装側の動きは続いています。webpackは5.109.0で本体にCSS Modulesの処理を取り込み、そのためにcss-loaderは2026年8月28日に非推奨になりました。Viteが内部で使うpostcss-modulesは2026年7月に9.0.1、typescript-plugin-css-modulesは10月24日に5.3.0を出しており、Next.js 16とVite 8も標準で対応しています。css-loaderの非推奨はCSS Modulesの終わりではなく、実装の置き場所がwebpack本体へ移ったという話です。
そのうえで、CSS Modulesを選ばないほうがよいのは次のような場合です。
- propsに応じてスタイルの値が大きく変わるUIが中心の場合。CSSカスタムプロパティで値を流し込むか、型付きでスタイルを生成するPanda CSSのような選択肢を検討します。
- デザイントークンに加え、共通のユーティリティクラスまで組織全体で統一したい場合。ユーティリティクラスの基盤が得意な領域です。設定方式の変化はTailwind CSS v3とv4の違い|破壊的変更と移行手順をv4.3.3の実ビルドで検証が判断材料になります。
- 画面が数枚の小さなサイト。クラス名が衝突しない規模で、ビルドの仕組みを増やす意味はありません。素のCSSで足ります。
逆にCSS Modulesが向くのは、コンポーネント単位のスコープが必要な中規模以上のアプリで、実行時のスタイル生成を持ち込みたくない構成です。CSS-in-JSの代表的なライブラリであるstyled-componentsはメンテナンスモードへの移行後も機能追加を続けており、その経緯はstyled-componentsとは?メンテナンスモード後の現在地にまとめています。TailwindとCSS-in-JSの組み合わせを比べるならTailwind CSSとEmotionの違い|v4での使い分けと共存が近い論点です。命名規則の逸脱を機械的に止めたい場合は、Stylelintとは?v17の変更点・導入手順・設定とCSSネスト対応【17.16対応】を合わせて入れると効きます。スタイル基盤の選定から業務システムを組み立てたい場合は、業務用・Webアプリ開発で相談を受け付けています。
よくある質問
CSS Modulesとは何ですか?
クラス名とアニメーション名がファイル単位のローカルスコープになるCSSファイルの仕組みです。バンドラがICSS(Interoperable CSS)という中間形式へ変換し、JavaScriptからインポートすると「元のクラス名→ビルド後の一意なクラス名」の対応表が返ります。ブラウザ標準ではなく、postcss-modulesやwebpack本体のCSS処理などの実装によって成り立っています。
CSS Modulesは非推奨ですか?
非推奨ではありません。仕様リポジトリの更新は2024年から止まっていますが、アーカイブはされていません。webpackは5.109.0から本体で.module.cssを処理し、Next.js 16.4.3とVite 8.3.5もどちらも設定なしで扱えます。2026年8月に非推奨になったのはcss-loaderというwebpack用の部品で、CSS Modulesそのものではありません。ただしNext.jsの公式ドキュメントは、大半をTailwind CSSで書き、足りない部分にCSS Modulesを使う順序で案内しています。
CSS ModulesとTypeScriptの関係は?
TypeScriptは.cssの中身を読めないため、.module.cssをインポートするには型宣言が必要です。ワイルドカードのdeclare moduleでビルドを通し、補完が欲しければtypescript-plugin-css-modules、誤字をtscで落としたければtyped-css-modulesなどでd.tsを生成します。TypeScript 7の言語サーバーはtsserverプラグインを読み込まないので、プラグインを使うならエディタだけTypeScript 6.0で動かします。
module.cssとmodule.scssの違いは何ですか?
スコープの仕組みは同じで、SCSSの前処理が入るかどうかだけが違います。.module.scssではSCSSの変数・mixin・ループが使えますが、Next.jsではsassパッケージの追加が必要です。ネストとカスタムプロパティはCSSで使えるので、mixinやループを使わないなら.module.cssのままでビルド依存を減らせます。
VS CodeでCSS Modulesの補完が効かないのはなぜですか?
確認すべき原因の1つは、エディタの言語サーバーがTypeScript 7になっていることです。TypeScript 7の言語サーバーはtsserverプラグインを読み込まないため、tsconfig.jsonに書いたプラグインは読み込まれず、そのことを知らせる警告も出ません。6系なら、VS Codeがワークスペース版のTypeScriptを使っているか、customMatcherと拡張子が一致しているかを確認します。