How to Fix the Error while Configuring CSS Modules with Webpack

I was racing through a small React prototype, eager to drop plain-old .css files and start using CSS Modules. A quick visit to webpack docs, a snappy the loader string, and <br> BAM my terminal lit up red.

If you’ve landed here because you saw the same “Invalid options object” wall of text, stick with me. I’ll walk you through the exact misstep, the fix, and four bite-sized improvements you can bolt on once everything compiles.

The Original webpack.config.js

path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
entry: './src/index.js',
output: {
path: path.join(__dirname, '/dist'),
filename: 'index_bundle.js',
publicPath: '/'
},
module: {
rules: [
{
test: /\.css$/,
loader:
'style-loader!css-loader?modules&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]'
},
{
test: /\.js$/,
exclude: /node_modules/,
use: { loader: 'babel-loader' }
}
]
},
devServer: { historyApiFallback: true },
plugins: [new HtmlWebpackPlugin({ template: './src/index.html' })]
};

The Scary Red Error

in ./src/index.css (...css-loader/dist/cjs.js?...!./src/index.css)
ValidationError: Invalid options object. CSS Loader has been initialised using an options object that does not match the API schema.
- options has an unknown property 'localIdentName'...
- options.importLoaders should be a boolean | number

What Those Cryptic Lines

What the validator saidWhat it’s telling me in plain English
unknown property localIdentNameThat option must now live inside a modules object.
importLoaders should be boolean | numberThe query-string turns 1 into the string "1", which the schema rejects.
overall “Invalid options object”My loader string is using webpack 4-era syntax, but I’m on css-loader v5 (or newer).

Why It Happens

  1. css-loader v4+ switched from query strings to real objects.
    Legacy shortcuts like ?modules still work, but most fine-grained flags were yanked.
  2. localIdentName moved under modules.
    modules can be true or an object that houses its own settings.
  3. Strings vs numbers.
    Query-string parameters are parsed as strings breaking the importLoaders type check.

The Clean Fix I Dropped In

{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 1, // 👉 number, not string
modules: {
localIdentName: '[name]__[local]___[hash:base64:5]'
}
}
}
]
}

I saved, restarted webpack-dev-server, and the build went from red to 100 % green.

Upgrade I Tried After It Work

These are mini-mods you can paste in and test in five minutes each.

Sass + CSS Modules

{
test: /\.scss$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
modules: { localIdentName: '[local]--[hash:6]' }
}
},
'sass-loader'
]
}

I created Button.scss, imported it inside React, and confirmed the classes were still scoped and hashed.

Autoprefixer with PostCSS

{
test: /\.css$/,
use: [
'style-loader',
{
loader: 'css-loader',
options: { importLoaders: 1, modules: true }
},
{
loader: 'postcss-loader',
options: {
postcssOptions: { plugins: ['postcss-preset-env'] }
}
}
]
}

After adding display: grid to a rule, Chrome DevTools showed automatic vendor prefixes nice!

Extract CSS for Production

MiniCssExtractPlugin = require('mini-css-extract-plugin');

{
test: /\.css$/,
use: [
process.env.NODE_ENV === 'production'
? MiniCssExtractPlugin.loader
: 'style-loader',
'css-loader'
]
}

plugins.push(
new MiniCssExtractPlugin({ filename: 'styles.[contenthash].css' })
);

Running NODE_ENV=production webpack spit out a separate, cache-friendly CSS file.

Instant Style HMR

: {
hot: true,
historyApiFallback: true
}

With HMR on, I tweaked a colour in index.css; the browser swapped styles in place—no full reload.

Exlain it Checklist

What I looked forCommand
Build passes—no ValidationErrornpm run start
DOM shows hashed class namesInspect element
PostCSS adds vendor prefixesCheck built CSS
Production build extracts CSSnpm run build

Final Thoughts

The root cause of my meltdown was simple: “old query string syntax meets new schema validation.” The moment I swapped in the modern use: [] array with a neat options object, webpack stopped shouting.

What I love about this little debugging detour is how many extras I picked up en route Sass, PostCSS, extraction, HMR. Each one slid neatly into the same rule pattern, reinforcing the muscle memory.

Related blog posts