CSS Loaders
Import CSS files directly into JavaScript — use css-loader to resolve CSS imports and style-loader to inject styles into the DOM.
CSS Loaders
One of Webpack's most powerful features is the ability to import CSS files directly into JavaScript. Instead of linking stylesheets in HTML, you import them in your entry point and Webpack handles the rest. In this episode you will configure css-loader and style-loader to process CSS files.
The Idea: CSS as a Module
// src/index.js
import './css/styles.css'; // Import CSS like a module!
import { greet } from './utils';
import { render } from './dom';
// ...rest of your code
Without loaders, this import would fail — Webpack does not know what to do with a .css file. We need two loaders to make it work.
Two Loaders, Two Jobs
| Loader | Job |
|---|---|
css-loader | Reads the CSS file, resolves @import and url() statements, and turns it into a JavaScript module |
style-loader | Takes the CSS from css-loader and injects it into the DOM as a <style> tag |
You need both. css-loader reads the CSS; style-loader injects it. Without css-loader, Webpack cannot parse the CSS. Without style-loader, the CSS is parsed but never applied to the page.
Step 1: Install the Loaders
npm install css-loader style-loader --save-dev
Step 2: Add the CSS Rule
Add a new rule to the module.rules array in webpack.config.js:
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
}
Understanding the Loader Chain
use: ['style-loader', 'css-loader']
// Execution order (right to left):
// 1. css-loader reads styles.css and resolves @import/url()
// 2. style-loader takes the result and injects it as a <style> tag
Remember: loaders in the use array execute from right to left (or bottom to top). css-loader runs first, then passes its output to style-loader. The order matters — reversing them would cause an error.
Step 3: Create a CSS File
src/css/styles.css
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Segoe UI', system-ui, sans-serif;
background: #1a1a2e;
color: #eee;
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
#app {
text-align: center;
padding: 40px;
background: #16213e;
border-radius: 16px;
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.3);
}
#app h1 {
font-size: 2.5rem;
margin-bottom: 12px;
background: linear-gradient(135deg, #667eea, #764ba2);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
#app p {
color: #9ca3af;
font-size: 1.1rem;
}
Step 4: Import the CSS
Add the CSS import at the top of src/index.js:
import './css/styles.css';
import { greet, VERSION } from './utils';
import { render } from './dom';
const message = greet('Webpack');
render('#app', `${message}
Version: ${VERSION}
`);
console.log('App loaded successfully!');
Step 5: Build and Test
npm run build
Open dist/index.html in a browser. The styles are applied even though there is no <link> tag in the HTML — style-loader injected them as a <style> tag at runtime.
If you inspect the page in DevTools, you will see a <style> element in the <head> containing your CSS — this was injected by style-loader when the JavaScript bundle executed.
How It Works Under the Hood
styles.css
↓
css-loader → Converts CSS to a JavaScript string
↓
style-loader → Injects that string into a <style> tag in the DOM
// The bundle contains something like:
var cssContent = "body { background: #1a1a2e; ... }";
var styleTag = document.createElement('style');
styleTag.textContent = cssContent;
document.head.appendChild(styleTag);
Extracting CSS to a Separate File
For production, injecting CSS via JavaScript is not ideal — the page flashes unstyled content until the JS loads. Use MiniCssExtractPlugin to extract CSS into a separate .css file:
npm install mini-css-extract-plugin --save-dev
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
// ...
module: {
rules: [
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader'],
},
],
},
plugins: [
new MiniCssExtractPlugin({
filename: 'styles.css',
}),
],
};
Replace style-loader with MiniCssExtractPlugin.loader. This extracts the CSS into a separate file (dist/styles.css) instead of injecting it via JavaScript. You then link it in your HTML normally.
style-loader vs MiniCssExtractPlugin
| Feature | style-loader | MiniCssExtractPlugin |
|---|---|---|
| How CSS is applied | Injected via JS at runtime | Separate .css file linked in HTML |
| Flash of unstyled content | Possible (CSS loads after JS) | No (CSS loads independently) |
| Hot Module Replacement | ✅ Works instantly | Requires page reload |
| Best for | Development | Production |
Updated webpack.config.js
const path = require('path');
module.exports = {
mode: 'development',
devtool: 'eval-source-map',
entry: './src/index.js',
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist'),
},
devServer: {
static: { directory: path.resolve(__dirname, 'dist') },
port: 3000,
open: true,
hot: true,
},
module: {
rules: [
{
test: /\.js$/,
exclude: /node_modules/,
use: {
loader: 'babel-loader',
options: { presets: ['@babel/preset-env'] },
},
},
{
test: /\.css$/,
use: ['style-loader', 'css-loader'],
},
],
},
};
Key Takeaways
- Webpack lets you import CSS into JavaScript — the CSS becomes part of the bundle
css-loaderreads and resolves CSS;style-loaderinjects it into the DOM as a<style>tag- Loaders execute right to left —
css-loadermust come afterstyle-loaderin the array style-loaderis great for development (HMR support);MiniCssExtractPluginis better for production- The CSS is bundled inside the JavaScript — no extra
<link>tag needed in HTML - For production, extract CSS to a separate file to avoid flash of unstyled content