Vite で CSS Modules のクラスが適用されない
Vite は *.module.css / *.module.scss のファイル名を CSS Modules として自動処理するが、命名が外れている、import から styles.foo を経由していない、css.modules.localsConvention のずれでケバブケース名が取れない、などで適用されないことがある。
公開: 更新:
要約
Vite で CSS Modules が効かない場合、ほぼ確実に ファイル名が *.module.css パターンになっていない か、JS 側で styles.foo を経由していない のどちらか。vite.config.ts で挙動を変えるのは命名規則の調整が必要な時のみで、まず命名と import 方法を確認する。
実行例
拡張子を .module.css に変えると出力 CSS のクラス名はハッシュ付きに変わるが、マークアップ側で生の button を当てたままだと両者が食い違う。styles.button 経由にすると一致し、localsConvention を入れるまでは styles.fooBar が undefined のままであることも確認できる。
$ npx vite build
vite v6.4.3 building for production...
transforming...
✓ 2 modules transformed.
rendering chunks...
computing gzip size...
dist/main.css 0.05 kB │ gzip: 0.06 kB
dist/main.js 0.07 kB │ gzip: 0.09 kB
✓ built in 28ms$ node dist/main.js
マークアップに付けるクラス: button$ cat dist/main.css
.button { color: red; }
.foo-bar { color: blue; }$ npx vite build
vite v6.4.3 building for production...
transforming...
✓ 2 modules transformed.
rendering chunks...
computing gzip size...
dist/main.css 0.07 kB │ gzip: 0.07 kB
dist/main.js 0.21 kB │ gzip: 0.18 kB
✓ built in 48ms$ node dist/main.js
クラス名の対応表: { button: '_button_1es0y_1', 'foo-bar': '_foo-bar_1es0y_2' }
マークアップに付けるクラス: button$ cat dist/main.css
._button_1es0y_1 { color: red; }
._foo-bar_1es0y_2 { color: blue; }$ npx vite build
vite v6.4.3 building for production...
transforming...
✓ 2 modules transformed.
rendering chunks...
computing gzip size...
dist/main.css 0.07 kB │ gzip: 0.07 kB
dist/main.js 0.26 kB │ gzip: 0.19 kB
✓ built in 48ms$ node dist/main.js
クラス名の対応表: { button: '_button_1es0y_1', 'foo-bar': '_foo-bar_1es0y_2' }
マークアップに付けるクラス: _button_1es0y_1
styles.fooBar: undefined$ npx vite build
vite v6.4.3 building for production...
transforming...
✓ 2 modules transformed.
rendering chunks...
computing gzip size...
dist/main.css 0.07 kB │ gzip: 0.07 kB
dist/main.js 0.28 kB │ gzip: 0.19 kB
✓ built in 45ms$ node dist/main.js
クラス名の対応表: { button: '_button_1es0y_1', fooBar: '_foo-bar_1es0y_2' }
マークアップに付けるクラス: _button_1es0y_1
styles.fooBar: _foo-bar_1es0y_2— 2026-09-18 時点の出力
検証環境
- 検証日
- 実行環境
node:20Debian GNU/Linux 12 (bookworm)- バージョン
- Node.js 20.20.2
- npm 10.8.2
- Python 3.11.2
- Git 2.39.5
この記事の「実行例」は、上記の環境で実際にコマンドを実行して得られた出力をそのまま掲載しています。 再現手順はリポジトリの検証スクリプトとして管理し、定期的に再実行して出力を更新しています。
よくある原因
- 拡張子ミス:
Button.cssのままだと通常 CSS として全コンポーネントにグローバル適用される - import 経由していない:
import './Button.module.css'のような副作用 import だけだとクラス名はハッシュ化されているので、そのまま書いたクラス名にはマッチしない - camelCase 期待:
.foo-barクラスをstyles.fooBarで取りたいのにlocalsConventionが camelCase 系になっていない - PostCSS の順序: 他のプラグインが先に走ると CSS Modules のハッシュ化結果が壊れる場合がある
解決策
1. 命名を .module.css にする
src/components/Button.module.css ← OK
src/components/Button.css ← 通常 CSSVite の Features ガイド(新しいタブで開く) のとおり、*.module.{css,scss,sass,less,styl,stylus,pcss,postcss,sss} がデフォルトの判定パターン。
2. JS 側で styles.xxx を経由する
import styles from "./Button.module.css";
export function Button() {
return <button className={styles.button}>OK</button>;
}副作用 import だけだと CSS は読み込まれるが、クラス名はビルド時にハッシュ化されているため <button className="button"> と書いても当たらない。
3. ケバブケース → camelCase 変換
Button.module.css で .foo-bar と定義したクラスを JS から styles.fooBar で取りたい場合:
// vite.config.ts
import { defineConfig } from "vite";
export default defineConfig({
css: {
modules: {
localsConvention: "camelCaseOnly",
},
},
});camelCaseOnly は元のケバブケース名を消すため、両方欲しい場合は camelCase を使う。
詳細は Shared Options のリファレンス(新しいタブで開く) の css.modules を参照。
4. PostCSS との競合
postcss.config.js で postcss-import などを使っている場合は CSS Modules 処理の前に解決させる。
module.exports = {
plugins: [
require("postcss-import"),
require("autoprefixer"),
],
};postcss-import を plugins 配列の先頭に置くと、@import 解決後に Vite が CSS Modules 化する。