CSS
There is no one single "standard" way of dealing with CSS in webpack. But there is no confusion about this fact either. Webpack knows nothing about CSS and there is no Ecma rule that describes how CSS file should/could be referenced from JavaScript. Yet, there are plenty of tools to deal with CSS processing for the sake of feature support, browser support, maintainability, etc. We'll cover one example from every main aria. That should be enough to extend the setup for any particular situation.
The setup contains 4 different ways to use CSS within a webpack project: plain CSS, CSS processors, CSS modules and CSS-in-JS. Chances are only one or two options are needed for the particular web app. Pick a preferred option and extend as needed.
Plain CSS
Let's start with the simplest option - support for the plain CSS files. For this option we are going to need following packages:
Dependencies
npm i css-loader style-loader mini-css-extract-plugin @types/mini-css-extract-plugin -D --save-exact
css-loader
- provides support for theimport
functionality for CSS files from JavaScript filesstyle-loader
- provides support for injecting CSS sources into the DOM (header tag)mini-css-extract-plugin
- provides support for extracting CSS sources into separate files@types/mini-css-extract-plugin
- mini-css-extract-plugin type definitions
Webpack configuration
First, we need to extend parts
file to make sure the rest of the webpack config files can reuse it.
webpack/parts.ts
import path from 'path'
import webpack, { Entry, Output, Node, Resolve, Plugin, RuleSetRule, Options } from 'webpack'
import HtmlWebpackPlugin from 'html-webpack-plugin';
import HtmlWebpackHarddiskPlugin from 'html-webpack-harddisk-plugin';
import { CleanWebpackPlugin } from 'clean-webpack-plugin'
export const distFolder = () => path.resolve(__dirname, '../dist')
export const getParts = () => ({
context: path.join(__dirname, '../src'),
entry: {
main: './index',
utils: './utils/index'
} as Entry,
output: {
path: path.resolve(distFolder(), 'js'),
filename: '[name].bundle.js',
publicPath: '/js/'
} as Output,
node: {
fs: 'empty'
} as Node,
resolve: {
extensions: ['.ts', '.js', '.json']
} as Resolve,
rules: [{
// Include ts/js files.
test: /\.(ts)|(js)$/,
exclude: [ // https://github.com/webpack/webpack/issues/6544
/node_modules/,
],
loader: 'babel-loader',
}] as RuleSetRule[] ,
plugins: [
new webpack.EnvironmentPlugin(['NODE_ENV']),
new HtmlWebpackPlugin({
chunksSortMode: 'dependency',
filename: '../index.html',
alwaysWriteToDisk: true
}),
new HtmlWebpackHarddiskPlugin(),
new CleanWebpackPlugin({
cleanOnceBeforeBuildPatterns: [
path.resolve(__dirname, '../dist/css/**/*'),
path.resolve(__dirname, '../dist/js/**/*')
],
verbose: true
})
] as Plugin[],
optimization: (cacheGroups?: { [key: string]: Options.CacheGroupsOptions }): Options.Optimization => ({
splitChunks: {
cacheGroups: {
vendors: {
test: /[\\/]node_modules[\\/]/i,
name: 'vendors',
chunks: 'all'
},
...cacheGroups || {}
}
},
runtimeChunk: {
name: 'vendors'
}
})
})
We have removed the module
key for the sake of the plain rules
array. We also made CleanWebpackPlugin delete content from explicitly specified folder list. Also, we changed optimization
to be a lambda that takes an optional object of key - value pairs for the potential cacheGroups
parameter extension.
webpack/webpack.config.dev.ts
For the webpack/webpack.config.dev.ts
, just some minor customizations are needed.
// ...
module: {
rules: [
...parts.rules,
{
test: /\.css$/,
use: ['style-loader', 'css-loader']
},
// ...
optimization: parts.optimization()
// ...
We changed the module
key to be explicit and made sure all the rules from the parts file are provided. As for the CSS itself, we have a new rule. It is applied to all the files with the css
extension. Those files are served by two loaders. In webpack, loaders are executed from right to left. css-loader
implements standard JavaScript import
and parses CSS file content, where style-loader
moves the content to the DOM. Actually, it creates a style
tag inside the head
tag.
webpack/webpack.config.ts
Change webpack/webpack.config.ts
:
import webpack, { Configuration } from 'webpack'
import MiniCssExtractPlugin from 'mini-css-extract-plugin'
import Environments from './environments.js'
import { getParts } from './parts'
const environments = Environments()
console.log(`Running webpack config for environment: ${environments.current}`)
const parts = getParts()
const config: Configuration = {
context: parts.context,
mode: 'production',
entry: parts.entry,
output: parts.output,
resolve: parts.resolve,
module: {
rules: [
...parts.rules,
{
test: /\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
}
]
},
node: parts.node,
plugins: [
...parts.plugins,
new webpack.SourceMapDevToolPlugin({
filename: '[name].js.map',
lineToLine: true
}),
new MiniCssExtractPlugin({
filename: '../css/[name].css',
chunkFilename: '../css/[name].css',
}),
],
optimization: {
...parts.optimization({
styles: {
name: 'styles',
test: /\.css$/,
chunks: 'initial',
enforce: true,
minSize: Infinity
}
}),
minimize: true,
removeAvailableModules: true,
}
}
export default config
Instead of the style-loader
used for the development flow, we substitute it with the MiniCssExtractPlugin
. This plugin is used to extract all CSS content into separate files, according to how CSS is imported from the JavaScript code. It also accounts for the code split files as well, so that CSS content will be loaded only when corresponding JavaScript chunk is loaded. Another change is around the optimization. We are adding another cache group for CSS files that represents a separate bundle.
name
- represents a chunk name (gets translated in to the file name for a chunk file)test
- regular expression to filter out files included in a chunk (*.css file only)chunks
- specifies what chunks are included into the bundle (initial
- static chunks,all
- static and dynamic chunks)enforce
- enforce bundle ruleminSize
- minimum size fo the bundle to be considered a separate file
minSize
is set up for Infinity. That means the min size rule will never be satisfied and a separate bundle file will never be created.style
cache group config is here only for the demo purposes. It shows an example of the technique to enforce chunk rules for CSS or any another content type. RemovingminSize
value setting creates a separate chunk file specifically for CSS content loaded from files rendered during first application load.
Example
src/app/index.css
body {
background-color: azure;
}
src/app/index.ts
import './index.css'
import { toCapital } from '@utils'
export type DynamicImport = Promise<{ default: () => string }>
///...
CSS Processors
CSS processors let you generate CSS code base on an enhanced CSS or alternative CSS syntax. There are many different processors and the actual choice depends on a level of experience and comfortably of a team and a feature set provided by the processor itself. For the rest of the explanation, PostCSS
is used but the steps to configure a different processor should be very similar, as long as an appropriate webpack loader exist such as sass-loader
for SASS
and less-loader
for LESS
.
Dependencies
npm i postcss-loader postcss-preset-env cssnano -D --save-exact
postcss-loader
- webpack loader for PostCSS processorpostcss-preset-env
- collection of PostCSS polyfills determined by the browser supportcssnano
- CSS compressor/optimizer
Webpack configuration
webpack/webpack.config.dev.ts
Alter webpack/webpack.config.dev.ts
content:
//...
rules: {
//...
{
test: /\.pcss$/,
use: ['style-loader', 'postcss-loader']
}
//..
}
//...
For the dev workflow, we introduced the rule for handling *.pcss
files with two loaders. First loader, postcss-loader
is similar to css-loader
in that it knows how to interpret JavaScript module import and content parsing for post-css files. From here, just as before, the content is passed to the style-loader
for further processing.
webpack/webpack.config.ts
Alter webpack/webpack.config.ts
content:
//...
rules: {
//...
{
test: /\.pcss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
}
//..
}
//...
For production, we are using postcss-loader
as well, but in order to place processed CSS content in separate files, MiniCssExtractPlugin
loader is needed. MiniCssExtractPlugin does not work directly with post-css loader, so we have to use css-loader
as an intermediary step.
PostCSS configuration
postcss.config.js
const postcssPresetEnv = require('postcss-preset-env');
const cssnano = require('cssnano');
const Environments = require('./webpack/environments');
const prodPlugins = [
cssnano({
preset: 'default'
})
];
module.exports = function(ctx) {
const environments = Environments(ctx.env);
return {
plugins: [
postcssPresetEnv({
features: {
'nesting-rules': true,
'color-mod-function': { unresolved: 'warn' }
}
})]
.concat(environments.isProduction ? prodPlugins : [])
};
}
PostCSS configuration mostly consist of plugins setup. In our case we have configured postcss-preset-env
to support CSS rules for nesting and the support of color-mod
function for color values definition. We also included CSS content optimizer cssnano
but only for production usage.
Example
src/app/feature/index.pcss
root {
--font-size: 30px;
}
li {
font-size: var(--font-size);
}
src/app/feature/index.ts
import './index.pcss'
export default (): string => 'feature'
CSS modules
Another useful technique of managing CSS content is called CSS Modules
. A CSS Module
is nothing but a CSS file with all the class names applicable to a local scope only. It's like a namespace for that particular file. It is achieved via a library support and a webpack build step. During webpack compilation, CSS file content is proceeded in the way that all the class names are substituted with unique randomly generated values scoped to the current file. Later, those values could be used within the JavaScript code as a way to refer to a particular CSS style definition.
Dependencies
So far, we have been using two flavors of CSS, plain and PostCSS enhanced. We can configure CSS modules for CSS or PostCSS files only, or for both. For plaint CSS, modules are supported by css-loader
, for PostCSS by postcss-loader
. There is only one additional dependency to be included.
npm i css-modules-typescript-loader -D --save-exact
css-modules-typescript-loader
- webpack loader that creates TypeScript definition files based for CSS module file content.
By default TypeScript compiler does not have any information on what content of the CSS module file is referenced by the code. This breaks TypeScript compilation step. We could have fallen back to require
method instead of Ecma import
syntax for CSS modules, but that is more of the workaround. To make TypeScript compilation go smooth, we are introducing another loader that reads CSS module file content and generates TypeScript definition file with all the exported class names, hence making the compiler step pass.
Webpack configuration
We already configured webpack to process *.css
and *.pcss
files. For CSS modules, different file extension is needed to prevent loaders collision. As a convention, we can use *.module.css
extension for plain CSS modules and *.module.pcss
for PostCSS modules.
webpack/webpack.config.dev.ts
Final content change for webpack/webpack.config.dev.ts
:
//...
module: {
rules: [
...parts.rules,
{
test: /\.css$/,
exclude: /\.module\.css$/,
use: ['style-loader', 'css-loader']
},
{
test: /\.pcss$/,
exclude: /\.module\.pcss$/,
use: ['style-loader', 'postcss-loader']
},
{
test: /\.module\.css$/,
use: [
'style-loader',
'css-modules-typescript-loader',
{
loader: 'css-loader',
options: {
modules: true,
}
}
]
},
{
test: /\.module\.pcss$/,
use: [
'style-loader',
'css-modules-typescript-loader',
{
loader: 'css-loader', options: { modules: true, importLoaders: 1 }
},
'postcss-loader'
]
}
]
}
//...
We introduced two additional rules, one for CSS modules - /\.module\.css$/
, another for PostCSS modules - /\.module\.pcss$/
. Webpack applies rules from top to bottom, so to prevent a rule from being applied to a different file type due to file extension collisions (ex. regex for CSS file includes searches for the regex for CSS module files), we have to extend existing rules. For CSS, we introduced exclude
clause to make sure it is not applied to CSS modules (exclude: /\.module\.css$/
). Same config change we applied to PostCSS rule. As for the CSS/PostCSS module rules, we use an appropriate loader that pipes content to the css-modules-typescript-loader
. css-modules-typescript-loader uses css-loader underneath, so all the options provided will be transferred to it. We use module: true
option for CSS modules and an additional importLoaders: 1
for PostCSS module.
importLoaders
option is a necessity for playing nicely with how loaders cooperate within webpack: https://github.com/webpack-contrib/css-loader/issues/228
webpack/webpack.config.ts
Final content for webpack/webpack.config.ts
:
//...
module: {
rules: [
...parts.rules,
{
test: /\.css$/,
exclude: /\.module\.css$/,
use: [MiniCssExtractPlugin.loader, 'css-loader']
},
{
test: /\.pcss$/,
exclude: /\.module\.pcss$/,
use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
},
{
test: /\.module\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-modules-typescript-loader',
{
loader: 'css-loader',
options: {
modules: true,
}
}
]
},
{
test: /\.module\.pcss$/,
use: [
MiniCssExtractPlugin.loader,
'css-modules-typescript-loader',
{
loader: 'css-loader', options: { modules: true, importLoaders: 1 }
},
'postcss-loader'
]
}
]
}
//...
For the production setup, situation is very similar. But instead of the style-loader we are using the loader provided by MiniCssExtractPlugin to extract generated CSS content into separate files.
Example
src/index.module.css
item-regular {
background-color: azure;
}
src/index.module.css.d.ts (auto-generated)
interface CssExports {
'item-regular': string;
}
declare var cssExports: CssExports;
export = cssExports;
src/index.ts
//...
import* as cssStyles from './index.module.css'
import * as pcssStyles from './index.module.pcss'
import * as styles from './index.style'
const f = func()
f.next()
let count = 0
const list = createList(styles.list)
list.className = styles.list
//...
CSS-in-JS
CSS modules technique was all about generating random names for CSS classes and being able to refer to them from JS code. CSS-in-JS pattern takes this approach to the next level. Instead of composing CSS by manipulating CSS code itself, it is composed using JavScript code. From now, CSS styles are defined in JavaScript as strings or objects with key - value pairs simulating CSS rules. So how does final CSS get created? It depends on when the library providing CSS-in-JS functionality executes the transformation to plain CSS code. If that happens in run time, we get style
tag in the head
of the page attached dynamically as components get rendered. If the transformation happens during the webpack compilation, generated code becomes part of a separate CSS file loaded by the browser during application execution.
For our setup, a library that provide dynamic CSS code generation during application runtime is used. If different approach is preferred, refer to the following list of libraries
Dependencies
npm i typestyle -D --save-exact
We will be using typestyle
for the sake of TypeScript support and zero configuration.
Example
src/index.style.ts
import { style } from 'typestyle'
export const list = style({
borderBottom: 'black 1px solid'
})
Linting
Just like we applied linting to TypeScript files, it could be applied to .css/.pcss files as well.
Dependencies
npm i stylelint stylelint-config-standard -D --save-exact
stylelint
- linting library for CSS codestylelint-config-standard
- standard rule set for stylelint
Configuration
.stylelintrc
{
"extends": "stylelint-config-standard",
"rules": {
"indentation": [4]
}
}
Add linting scripts to package.json
"lint": "npm run lint:sources && npm run lint:styles",
"lint:sources": "eslint './src/**/*.ts' --max-warnings=0",
"lint:fix:sources": "npm run lint:sources -- --fix",
"lint:styles": "stylelint 'src/**/*.css' 'src/**/*.pcss'",
"lint:fix:styles": "npm run lint:styles -- --fix",