How to Fix the Webpack CSS Module Error Getting My Styles to Load

Below is the original configuration you posted, the exact error message, why it happens, and a step-by-step fix. I’ll finish with a “practice” section that lets you try out a few extra Webpack tricks (CSS Modules, Sass, Autoprefixer, and Hot Reload) so you can see how each piece slots in.

Original Code

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const NodemonPlugin = require('nodemon-webpack-plugin');
require('file-loader');

module.exports = {
mode: process.env.NODE_ENV || 'production',
entry: './src/index.js',
module: {
rules: [
{
test: /\.(png|jpe?g|gif)$/i,
use: ['file-loader'],
},
{
test: /\.css$/i,
use: ['style-loader'], // ← only one loader here
}
]
},
output: {
filename: 'main.js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new HtmlWebpackPlugin(),
new NodemonPlugin()
]
};

Error as seen in the console

ERROR in ./src/component/list-view/list-view.css 1:3
Module parse failed: Unexpected token (1:3)
You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.
> h2 {
| color: #ff0000;
| }

What Actually Broke

I had added only style-loader. That loader’s entire job is to inject a string of CSS into the <head> at runtime.
What it doesn’t do is turn a real .css file into that string. That’s css-loader’s territory.

Webpack applies loaders right to left (last loader runs first). So the correct stack is:

{
test: /\.css$/i,
use: ['style-loader', 'css-loader'] // css-loader on the right!
}

One npm i -D css-loader, one save, and the build finally compiled.

Breathing Room:

A green build was nice, but I wanted more:

  • Separate CSS file in production (goodbye Flash of Unstyled Content).
  • Local-scoped class names so my component styles don’t collide.
  • Sass support for variables and nesting.
  • Autoprefixer so I never type -webkit- again.
  • A dev server with hot reload so I can edit and see changes instantly.

That led me to the “practice playground” below.

Extra Dev Dependencies

i -D css-loader style-loader            \
mini-css-extract-plugin \
sass sass-loader \
postcss postcss-loader autoprefixer\
webpack-dev-server

The Playground Config

// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
mode: process.env.NODE_ENV || 'development',
entry: './src/index.js',
devtool: 'source-map',

devServer: {
static: path.resolve(__dirname, 'dist'),
port: 3000,
hot: true,
open: true
},

module: {
rules: [
/* Images & fonts */
{ test: /\.(png|jpe?g|gif|svg|woff2?|eot|ttf|otf)$/i, type: 'asset/resource' },

/* Global CSS */
{
test: /\.css$/i,
exclude: /\.module\.css$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
{
loader: 'postcss-loader',
options: { postcssOptions: { plugins: ['autoprefixer'] } }
}
]
},

/* CSS Modules */
{
test: /\.module\.css$/i,
use: [
MiniCssExtractPlugin.loader,
{ loader: 'css-loader', options: { modules: true } }
]
},

/* Sass */
{
test: /\.s[ac]ss$/i,
use: [
MiniCssExtractPlugin.loader,
'css-loader',
'sass-loader'
]
}
]
},

output: {
filename: 'main.[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true
},

plugins: [
new HtmlWebpackPlugin({ template: './public/index.html' }),
new MiniCssExtractPlugin({ filename: 'styles.[contenthash].css' })
]
};

What Each Upgrade Gives Me

FeatureWhy I Like It
MiniCssExtractPluginEmits a real styles.css so the browser can cache it and show styles immediately.
Source mapsWhen something breaks, DevTools points at the real file, not main.js.
Dev server + HMRSave a file, see the page refresh or even swap modules instantly.
asset/resourceCopies images and fonts to /dist without the old file-loader.
CSS ModulesI can write .title {} in two components and they’ll never clash.
SassVariables, nesting, and math right in my stylesheets.
AutoprefixerVendor prefixes appear (or don’t) based on my browserslist targets.

Quick Drills to Lock It In

  • Toggle CSS Modules
    Rename list-view.css to list-view.module.css, then:
styles from './list-view.module.css';
document.body.innerHTML = `<h2 class="${styles.title}">Hello Modules</h2>`;
  • Play with Sass
    Create styles/variables.scss with $brand: #6EC1E4; and import it. Watch the variable compile.
  • Autoprefixer Check
    Add display: flex in any stylesheet → build → open generated CSS. See the prefixes (or lack thereof) for yourself.
  • Break-then-Fix
    Comment out css-loader, rebuild, watch Webpack explode, then restore it. Nothing hammers the concept home faster.

Final Thoughts

That single missing loader taught me more about Webpack’s loader chain than any tutorial. Once I understood that every asset travels through a pipe of loaders, right-to-left, the whole ecosystem clicked:

  • Need to transform a file? Add a loader on the right.
  • Need to inject or extract the result? Add a loader on the left.
  • Want fancy extras like Sass or Autoprefixer? Just keep stacking pipes.

Now my build spits out a clean main.[hash].js and styles.[hash].css, hot-reloads every time I hit save, and my component styles stay safely scoped in their own little worlds. Most important, I know why each piece is there—so the next error message feels like a puzzle, not a brick wall.

Related blog posts