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
Feature | Why I Like It |
---|---|
MiniCssExtractPlugin | Emits a real styles.css so the browser can cache it and show styles immediately. |
Source maps | When something breaks, DevTools points at the real file, not main.js . |
Dev server + HMR | Save a file, see the page refresh or even swap modules instantly. |
asset/resource | Copies images and fonts to /dist without the old file-loader . |
CSS Modules | I can write .title {} in two components and they’ll never clash. |
Sass | Variables, nesting, and math right in my stylesheets. |
Autoprefixer | Vendor prefixes appear (or don’t) based on my browserslist targets. |
Quick Drills to Lock It In
- Toggle CSS Modules
Renamelist-view.css
tolist-view.module.css
, then:
styles from './list-view.module.css';
document.body.innerHTML = `<h2 class="${styles.title}">Hello Modules</h2>`;
- Play with Sass
Createstyles/variables.scss
with$brand: #6EC1E4;
and import it. Watch the variable compile. - Autoprefixer Check
Adddisplay: flex
in any stylesheet → build → open generated CSS. See the prefixes (or lack thereof) for yourself. - Break-then-Fix
Comment outcss-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.