webpack + postcss + cssnext

Obligatory preamble
Iām a Sass man (Ė¢įµį¶¦įµįµįµįµįµįµįµįµįµįµįµįµįµ). Iāve been hooked on Sass for over 5 years, and I still love it. This isnāt an everybody-should-use-PostCSS post, because Sass still holds some advantages, and despite what Medium tells you, your stack doesnāt need to be rewritten every other month. However, I do gain several things from the switch.
First, I get to abandon a proprietary Sass syntax ($color-blue: #32c7ff;
) in favor of a
standards-backed format (--color-blue: #32c7ff;
). I can actually write CSS again, and not an
abstracted substitute. This is not only far more future-proof, but as CSS evolves, Iām taking
advantage of all the newest features (which also lets you do
cool stuff like this).
Secondāand this is beyond the scope of this postāby writing more modular CSS, Iām less limited in how I load those styles, either directly from the browser or in JS.
Third, PostCSS boasts some really fast compile times (up to 36Ć faster than Ruby Sass). While this isnāt reason enough to use a technology, it sure is a nice bonus once youāve decided to switch.
CSS is undergoing a much subtler shakeup than JavaScript has been for the past year and a half. But itās still being shaken up, nonetheless, as the way we write markup and style is changing due to the former.
The setup
Note: Iām using ExtractTextPlugin to save the CSS as a separate file; you can use
_style-loader_
instead if youāre loading CSS with JS.
Command Line
Add webpack, css-loader, file-loader (weāre not using it directly, but itās a dependency), PostCSS, postcss-loader, cssnext, and postcss-import. Run the following from your command line:
yarn add --dev webpack extract-text-webpack-plugin css-loader file-loader postcss postcss-loader postcss-cssnext postcss-import
Also install webpack globally (npm i -g webpack
) if you havenāt alreadyāitāll make compiling
simpler unless you
set up NPM scripts.
webpack.config.js
Weāll add something like this to webpack.config.js
(JS config largely omitted):
const webpack = require('webpack');
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
module.exports = {
context: path.resolve(__dirname, 'src'),
entry: {
app: './app.js';
},
module: {
loaders: [
{
test: /\.css$/,
use: ExtractTextPlugin.extract({
use: [
{
loader: 'css-loader',
options: { importLoaders: 1 },
},
'postcss-loader',
],
}),
},
],
},
output: {
path: path.resolve(__dirname, 'dist/assets'),
},
plugins: [
new ExtractTextPlugin('[name].bundle.css'),
],
// ā¦
};
Note that weāve set importLoaders: 1
on css-loader
. Weāre setting this because we want PostCSS
to git @import
statements first
(longer explanation here).
postcss.config.js
This is just my personal preference: using a global postcss.config.js
file in the project root.
Sure, you can configure this all in webpack, and some may find that more convenient. But I find this
method to be a little less hassle. Create a postcss.config.js
file in your project root:
module.exports = {
plugins: {
'postcss-import': {},
'postcss-cssnext': {
browsers: ['last 2 versions', '> 5%'],
},
},
};
Pay attention to how postcss-import
comes first. This is the difference between cssnext
working or not. What this does is resolve @import
statements in your CSS first, and then take
all your CSS in as one file and run all of it with cssnext. If you swapped the order, cssnext would
only process your entry file and none of your @import
ed files would be autoprefixed/transpiled.
The browsers
option is for Autoprefixer, which comes
built-in. If you havenāt used this, this prevents you from writing browser prefix-less CSS.
This__ is how your CSS should be. If you know what a
[browserslist](https://github.com/ai/browserslist)
file is
and youāre using one already, you can actually use that instead of declaring that here.
Developing
With the setup complete, weāll start a file at src/app.js
:
import styles from './app.css';
And our src/app.css
file can look something like this:
/* Shared */
@import 'shared/colors.css';
@import 'shared/typography.css';
/* Components */
@import 'components/Article.css';
With cssnext, we can write stuff like:
/* shared/colors.css */
:root {
--color-black: rgb(0, 0, 0);
--color-blue: #32c7ff;
}
/* shared/typography.css */
:root {
--font-text: 'FF DIN', sans-serif;
--font-weight: 300;
--line-height: 1.5;
}
/* components/Article.css */
.article {
font-size: 14px;
& a {
color: var(--color-blue);
}
& p {
color: var(--color-black);
font-family: var(--font-text);
font-weight: var(--font-weight);
line-height: var(--line-height);
}
@media (width > 600px) {
max-width: 30em;
}
}
And cssnext takes all of our variables, nested selectors, media queries, and all our other stuff
(read the docs to see all it supports) and exports it out to a
dist/assets/css.bundle.css
whenever we run webpack
or webpack -p
:
.article {
font-size: 14px;
}
.article a {
color: #32c7ff;
}
.article p {
color: rgb(0, 0, 0);
font-family: 'FF DIN', sans-serif;
font-weight: 300;
line-height: 1.5;
}
> @media (min-width: 601px) {
.article {
max-width: 30em;
}
}
And thanks to webpack, we still get all of the great module splitting and tree shaking.
Kicking it Up a Notch

I had a more demanding use case where I needed to load my webfont stack with one CSS manifest (separate from my other styles, for performanceāsimilar to @zachleatās Asynchronous Data URI font-loading method or Google Fonts, minus the subsetting & browser sniffing). The advantage of this is two-fold:
- It reduces 5 webfont header requests into 1 CSS request, optimizing asset delivery
- This project used a CDN, which means webfont files wonāt load cross-domain anyway
My stylesheet lives in ./src/App.css
, and I declared all my @font-face
rules in
./src/Font.css
. This is what my webpack.config.js
file looked like (_note I was also using
_url-loader_
ā __yarn add --dev url-loader_
):
const webpack = require('webpack');
const path = require('path');
const ExtractTextPlugin = require('extract-text-webpack-plugin');
const extractStyles = new ExtractTextPlugin('app.css');
const extractFonts = new ExtractTextPlugin('fonts.css');
module.exports = {
context: path.resolve(__dirname, 'src'),
entry: {
app: './App.js',
},
module: {
loaders: [
{
test: /App\.css/,
use: extractStyles.extract({
use: [
{
loader: 'css-loader',
options: { importLoaders: 1 },
},
'postcss-loader',
],
}),
},
{
test: /Font\.css/,
use: extractFonts.extract({
use: 'css-loader',
}),
},
{
test: /\.(woff|woff2)$/,
use: ['url-loader'],
},
],
},
output: {
path: path.resolve(__dirname, 'dist/assets'),
filename: '[name].bundle.js',
},
resolve: {
modules: [path.resolve(__dirname, 'src'), 'node_modules'],
},
plugins: [extractStyles, extractFonts],
};
My ./src/Font.css
file isnāt anything special, but just for clarity:
@font-face {
font-family: 'FF DIN';
font-weight: 400;
font-style: normal;
src: url('./assets/ffdin-regular.woff');
}
/* ⦠More fonts like this */
In my ./src/App.js
file, two lines connect the remaining dots:
import styles from './App.css';
import fonts from './Font.css';
Now when I run webpack -p
it will export 2 files (in addition to my JS):
./dist/assets/styles.css
./dist/assets/fonts.css
Note: I specified my output filenames up at the top of the file with _const exportStyles_
and
_const _``_exportFonts_
. Theyāll go in the same folder as _output.path_
, along with
your JS. No, it doesnāt make much sense to me, either, but there you have it.
This setup is rightfully confusing, and Iāll admit someone smarter than me can come up with a
smarter configāif, for example, I changed the name of App.css
or Font.css
, Iād have to update
this config, too.
But regardless, the most important thing to grasp is I have 2 separate new ExtractTextPlugin
instancesāone for styling and one for fonts. You need one **new ExtractTextPlugin**
instance
for every separate CSS bundle you want. If you tried to use the same instance for both, the plugin
would simply lump those both together.
Lastly, you can see Iām handling the base64 embedding of .woff
and .woff2
files with
url-loader
. If I didnāt want to embed the file, I could use
[file-loader](https://github.com/webpack/file-loader)
instead. Iām only loading those 2 formats
because itās 2017, and thatās all you need for modern browsers now.
Result
Iām extremely happy with the end result of this, because not only do I have my webfonts loading asynchronously, but my CSS is also bundled separately. I get to have all the benefits of using webpack, but my app is also progressive and still works just fine even if JavaScript is disabled on the client end.
Caveats
cssnext isnāt a 1:1 replacement for Sass, so be sure to temper your expectations before committing and diving in. Principally among the features missing are Sassās powerful function and mixins abilities. Plugins like PreCSS attempt to bridge the gaps and allow Sass syntax in the PostCSS ecosystem. But PreCSS isnāt a perfect replacement, and there may invariably be one or two things you need to modify with a Sass codebase to get it working.