December 18, 2018 · postcss webpack phoenix elixir

PostCSS in Phoenix 1.4 with Webpack

The Phoenix Framework now ships with webpack instead of Brunch for asset generation. I was excited when I read this announcement in the Phoenix 1.4 release notes because webpack is proving to be a standard in the circles I work. Rails made a similar move last year with the addition of Webpacker in Rails 5.1. So, while it was possible before to manually shim webpack into Phoenix, I appreciate the consensus and having it available by default with mix phx.new.

Fundamentally webpack takes in directories of files, runs them through a series of tasks/loaders (concatenation, minification, pre-processing, etc.), then outputs the results to a build directory. It is similar to what the Rails Asset Pipeline does with Sprockets but with more power (and admittedly befuddling config).

The default Phoenix webpack setup does not make any assumptions about your CSS or JS preferences. It only supports vanilla CSS out of the box, so we’ll use the webpack plugin system to extend this out to support PostCSS.

What PostCSS?

PostCSS is a great CSS preprocessor that layers on top of vanilla CSS using plugins. It lets you write future CSS syntax (using the PostCSS Preset Env plugin) which it then transpiles to what’s currently supported by browsers. This serves the same purpose for CSS that Babel does for JavaScript in transpiling ES6 to widely supported JavaScript.

I originally heard about PostCSS through one of its plugins: Autoprefixer. Autoprefixer developed during a time when browsers were releasing proprietary prefixed properties for emerging CSS features. For instance, to apply a box-shadow you also needed to include -moz-box-shadow, -webkit-box-shadow, and a weird filter/-ms-filter thing for IE. Autoprefixer came as a huge relief to all that; just write the standard CSS rule, and it would take care of any variants.

Install PostCSS (and loader) using NPM

PostCSS is available as a package on NPM. We’ll need both that and the PostCSS Loader to tie it into webpack. To clarify, “loaders” are essentially tasks/plugins for webpack.

Install PostCSS and PostCSS Loader using NPM:

cd your_project/assets
npm install postcss-plugin postcss-loader --save-dev

NOTE: Phoenix places the package.json and node_modules directory in an assets subfolder, not at the root level.

Configure PostCSS

Add PostCSS Loader to the CSS rules in webpack.config.js:

// assets/webpack.config.js
module.exports = (env, options) => ({
  …
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader'
        }
      },
      {
        test: /\.css$/,
-       use: [MiniCssExtractPlugin.loader, 'css-loader']
+       use: [MiniCssExtractPlugin.loader, 'css-loader', 'postcss-loader']
      }
    ]
  }
});

Then create a new postcss.config.js in the assets directory:

// assets/postcss.config.js
module.exports = {
  plugins: {
    "postcss-plugin": {}
  }
}

Add a PostCSS plugin

With the PostCSS plugin installed we can still only write vanilla CSS. To do anything special we need to install plugins. This is different from Sass which is a complete solution within a single library.

Looking at the PostCSS plugins page, there are so many that it can be difficult to know where to start. I like PreCSS as a jumping off point. It’s a curated composite plugin that configures an experience similar to Sass + Autoprefixer.

Install the PreCSS plugin using NPM:

npm install precss --save-dev

Then add the plugin to your postcss.config.js:

// assets/postcss.config.js
module.exports = {
  plugins: {
    "postcss-plugin": {},
+   "precss": {}
  }
}

Import Normalize.css using NPM

Another thing I often need to do is import a CSS dependency from NPM, such as Normalize.css, then prepend it to my stylesheets. To import files, we’ll reach for PostCSS Import which adds @import statements:

Install the postcss-import and normalize.css plugins using NPM:

npm install postcss-import normalize.css --save-dev

Then add the PostCSS Import plugin to your postcss.config.js:

// assets/postcss.config.js
module.exports = {
  plugins: {
+   "postcss-import": {},
    "postcss-plugin": {},
    "precss": {}
  }
}

NOTE: The Import plugin must come first because of how the files are processed.

Now you can import Normalize.css in your CSS:

// assets/css/app.css
@import "normalize.css";

Why PostCSS?

The end result is that we’ve created something that works very similar to Sass + Autoprefixer. So why even bother? My favorite aspect of PostCSS is its extensibility. There are plugins to handle everything from image optimization and inlining to layouts, grids, and colors. I also like that I can write current/future CSS and have it automatically transpiled to work everywhere. We don’t yet have CSS variables in all browsers, but someday we (maybe) will, and I can use them now while I wait. So pipe-dream is to write PostCSS, then one day remove the plugin all-together once all the features are supported.

Are there downsides? You bet! The best part of using Sass is that it is a single library with batteries included. If you’re trying to look up how to lighten a color variable, you can reference the API docs and be done. With PostCSS you are free to choose any color library, but then later need to remember which one it was and how to use it. PostCSS also increases the number of project dependencies because of its modular architecture, though that goes for pretty much the entire JavaScript ecosystem, so *shrug*.