Episode 5 of 6

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

LoaderJob
css-loaderReads the CSS file, resolves @import and url() statements, and turns it into a JavaScript module
style-loaderTakes 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

Featurestyle-loaderMiniCssExtractPlugin
How CSS is appliedInjected via JS at runtimeSeparate .css file linked in HTML
Flash of unstyled contentPossible (CSS loads after JS)No (CSS loads independently)
Hot Module Replacement✅ Works instantlyRequires page reload
Best forDevelopmentProduction

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-loader reads and resolves CSS; style-loader injects it into the DOM as a <style> tag
  • Loaders execute right to left — css-loader must come after style-loader in the array
  • style-loader is great for development (HMR support); MiniCssExtractPlugin is 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