How Angular-CLI and Webpack Deliver Your CSS to the Browser
After running ng new myapp, you'll have a fresh Angular project with a standard directory layout:
src
app
assets
index.html
styles.css
...
For our purposes, the relevant file is styles.css, which serves as the home for global styling rules. You have two options for populating it: write styles directly or bring in other stylesheets using CSS-specific import statements. Suppose your index.html contains the following:
<body>
<header>
<span>I am header span</span>
</header>
<span>I am regular span outside of header</span>
</body>
And your global styles.css has these rules:
@import "header.css";
span {
color: green;
}
While header.css holds these:
header span {
color: blue;
}
When you launch the app, you'll see the styling applied as shown below:

That outcome is exactly what you'd expect, but the mechanism behind it is worth examining. The heavy lifting is done by webpack, which Angular-CLI configures but doesn't replace. Angular-CLI simply wraps webpack and provides sensible defaults.
This walkthrough will demonstrate how webpack accomplishes this task. Even if you're not using Angular-CLI, the concepts here apply to any webpack-based project.
Project Setup
Let's begin by scaffolding a new project with this layout:
app
header.css
styles.css
index.html
Since we're focusing exclusively on CSS, no JavaScript is needed. The app directory contains a stylesheet for the header component. We'll pull it into the main style.css using a CSS import.
Here's what index.html will look like:
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<header>
<span>I am header span</span>
</header>
<span>I am regular span outside of header</span>
</body>
Notice there are two span elements: one nested inside the header and another at the top level of the body. Our goal is to render the header's span in blue and the body-level span in green.
To achieve that, we'll place the header-specific rules in app/header.css:
header span {
color: blue;
}
The global styles go into styles.css:
@import "app/header.css";
span {
color: green;
}
Now that we have our stylesheets ready, we need to get webpack involved. First, install webpack as a dependency:
npm i webpack --save-dev
Angular-CLI ships these styles as a dedicated bundle, and we'll replicate that behavior. Since webpack generates bundles from entry points, we'll define a separate one for our CSS in the configuration file.
const path = require('path');
module.exports = {
entry: {
styles: "./styles.css"
}
};
We also need to specify where the output should be written. Let's add that detail:
const path = require('path');
module.exports = {
entry: {
styles: "./styles.css"
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: "[name].js"
}
};
With the basic configuration in place, running webpack will produce this error:
Module parse failed: D:\medium\styles.css\styles.css… You may need an appropriate loader to handle this file type.
Webpack treats every file as a JavaScript module, and styles.css obviously doesn't qualify. To convert CSS into a JS module, we turn to loaders. The official documentation describes them this way:
Loaders are transformations that are applied on the source code of a module. They allow you to pre-process files as you
importor "load" them… Loaders can transform files from a different language (like TypeScript) to JavaScript, or inline images as data URLs.
That's precisely what we're looking for. The CSS loader already exists in the webpack ecosystem and handles this transformation.
Working with the CSS Loader
Many loaders ship as separate packages, so let's install this one:
npm install --save-dev css-loader
After installation, we register it in our configuration:
module.exports = {
entry: ...,
output: ...,
module: {
rules: [
{
test: /\.css$/,
use: ['css-loader']
}
]
}
The test property takes a regexp—here it's /\.css$/—which webpack applies to every file it processes. When a file matches that pattern, the loaders in the use array process its contents. This particular pattern catches any file ending in .css.
Now let's run webpack and check the output:
$ webpack
Hash: 5e39e00b22e7d8dbb305
Version: webpack 3.4.1
Time: 698ms
Asset Size Chunks Chunk Names
styles.js 5.25 kB 0 [emitted] styles
[1] ./styles.css 272 bytes {0} [built]
[2] ./node_modules/css-loader!./app/header.css 200 bytes {0} [built]
+ 1 hidden module
From the output, we can see it created a styles.js bundle in about 698 ms. That bundle contains the styles.css and /app/header.css modules, plus one dependency from node_modules. By default, webpack labels modules originating from ["node_modules", "bower_components", "jam", "components"] as hidden. To reveal what that hidden module is, run this command:
webpack --display-modules
It turns out to be a utility supplied by the css-loader:
$ webpack — display-modules
Hash: 1229210b090997ed5ae2
Version: webpack 3.4.1
Time: 417ms
Asset Size Chunks Chunk Names
styles.js 5.26 kB 0 [emitted] styles
[0] ./node_modules/css-loader/lib/css-base.js 2.26 kB {0} [built]
[1] ./styles.css 274 bytes {0} [built]
[2] ./node_modules/css-loader!./app/header.css 201 bytes {0} [built]
So webpack has generated styles.js in the dist directory. Let's include that file in index.html and see if the styling takes effect—green above, blue inside the header:
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="dist/styles.js"></script>
</head>
<body>
<header>I am header</header>
<span>I am regular span outside of header</span>
</body>
But when we look at the result:

Nothing happened. The text is still black.
Let's peek inside the generated styles.js. Here's an abbreviated view:
/* 1 */
(function (module, exports, __webpack_require__) {
exports = module.exports = __webpack_require__(0)(undefined);
exports.i(__webpack_require__(2), "");
exports.push([module.i, "span {\r\n color: green;\r\n}", ""]);
}),
/* 2 */
(function (module, exports, __webpack_require__) {
exports = module.exports = __webpack_require__(0)(undefined);
exports.push([module.i, "header span {\r\n color: blue;\r\n}", ""]);
})
The core pattern is that each module simply exports its styles as a string. Once both modules run, we're left with an array like this:
exportedStyles = [
// specified in the styles.css
"span {\r\n color: green;\r\n}",
// imported from the app/header.css
"header span {\r\n color: blue;\r\n}"
];
But nothing uses that array yet. We still need something to consume it, inject the rules into a style tag on the page, giving us what's shown here:
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="dist/styles.js"></script>
<style>
span { color: green; }
header span { color: blue; }
</style>
As you might have guessed already, there's a loader specifically for this job.
Integrating the Style Loader
That package is separate too, so let's get it installed:
npm install style-loader --save-dev
Similar to css-loader, we add this to the webpack configuration:
module.exports = {
entry: ...,
output: ...,
module: {
rules: [
{
test: /\.css$/,
use: [
"style-loader",
"css-loader"
]
}
]
}
You'll notice that style-loader appears before css-loader in the array. Webpack processes loaders in reverse order, meaning css-loader runs first, followed by style-loader. This ordering is intentional. The css-loader converts CSS into a JS module that exports strings, and then style-loader takes those strings and inserts them into a <style> element in the document.
Let's rerun webpack and observe the result:
Hash: 9fe17c2f175614de78ea
Version: webpack 3.4.1
Time: 556ms
Asset Size Chunks Chunk Names
styles.js 18.1 kB 0 [emitted] styles
[1] ./styles.css 999 bytes {0} [built]
[2] ./node_modules/css-loader!./styles.css 274 bytes {0} [built]
[3] ./node_modules/css-loader!./app/header.css 201 bytes {0} [built]
+ 3 hidden modules
This time the build completes without errors. Opening index.html now gives us the styling we originally wanted:

One issue remains, though: we're still linking styles.js manually in index.html. Can webpack handle that automatically? It can, thanks to a plugin—specifically HtmlWebpackPlugin.
Using HTML Webpack Plugin
The official documentation explains its purpose:
…simplifies creation of HTML files to serve your webpack bundles. This is especially useful for webpack bundles that include a hash in the filename which changes every compilation. You can either let the plugin generate an HTML file for you, supply your own template using lodash templates or use your own loader.
Time to see it in action. Install the plugin:
$ npm install — save-dev html-webpack-plugin
By default, this plugin produces its own index.html. Since we already have one, we can tell it to use ours via the template option, pointing to the existing file:
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {...},
output: {...},
module: {...},
plugins: [
new HtmlWebpackPlugin({
template: "./index.html"
}
)
]
};
After rebuilding, here's what the generated index.html in dist looks like—created by the plugin now:
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<header>
<span>I am header span</span>
</header>
<span>I am regular span outside of header</span>
<script type="text/javascript" src="styles.js"></script>
</body>
The plugin has automatically appended styles.js as the final element inside body.
Below is the full webpack.config.js for this project:
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
module.exports = {
entry: {
styles: "./styles.css"
},
output: {
path: path.resolve(__dirname, 'dist'),
filename: "[name].js"
},
module: {
rules: [
{
test: /\.css$/,
use: [
"style-loader",
'css-loader'
]
}
]
},
plugins: [
new HtmlWebpackPlugin({
template: "./index.html"
}
)
]
};
Angular-CLI's Additions
Beyond what we've set up here, Angular-CLI extends the pipeline with loaders for SASS, LESS, and STYLUS files, each handled by its own dedicated loader. The CLI also chains several postcss plugins—including postcss-url, autoprefixer, and cssnano—through the postcss-loader:
const postcssPlugins = function () { .... };
...
{
"loader": "postcss-loader",
"options": {
"plugins": postcssPlugins
}
},
Source Code
The complete setup is available in a GitHub repository here, in case you'd like to experiment with it.
