Smarter CSS builds with Webpack

Nobody writes CSS in one big file any more.

You, the savvy CSS developer, well-versed in SMACSS, SUIT and BEM, are writing small, isolated modules in separate files.

stylesheets/
config/
colors.sass
media_queries.sass
modules/
btn.sass
dropdown.sass
header.sass
utilities/
align.sass
clearfix.sass

When it comes to building a single bundle.css file to send down to your users, you're manually specifying all of the individual files that you know your app needs.

If you're using a preprocessor like Sass, you might be using its @import statement:

@import "vendor/normalize"
@import "config/colors"
@import "config/media_queries"
@import "modules/btn"
@import "modules/dropdown"
@import "modules/header"
@import "utilities/align"
@import "utilities/clearfix"

If you're working with Rails or Middleman, you might be using Sprockets' //= require directives:

//= require vendor/normalize
//= require_tree ./modules
//= require_tree ./utilities

Or you might have rolled your own asset pipeline, using a tool like Gulp or Grunt to collect, process & concatenate the individual files:

gulp.task('styles', function () {
gulp.src('styles/**/*.scss')
.pipe(sass())
.pipe(gulp.dest('./tmp'))
.pipe(concat('bundle.css'))
.pipe(autoprefixer())
.pipe(gulp.dest('./build'));
});

All of these approaches require you to know which bits of CSS your app actually uses. You're tediously maintaining a list of dependencies in a manifest file, or (more likely) glob importing entire directories.

You're potentially including CSS that isn't actually required by your app. You only discover & remove unused CSS by occasionally searching your project's HTML templates for a class name (or having a tool do it for you).

You're keeping your HTML's CSS dependencies in your head and not in code. In my humble opinion, this is a Very Bad Thing.

But never fear... if you're generating HTML from modular views written in JavaScript, there's a better way! (If you're not, here's a sneak peek of the future!)

Requiring UI dependencies with Webpack

Webpack, the amazing module bundling Swiss army knife that you should probably start using, is all about letting you write modular UI code with explicitly declared dependencies.

You've probably used CommonJS modules before:

var _ = require("underscore");
var findTastiestPizza = function (pizzas) {
return _.find(pizzas, function (pizza) {
return pizza === "hawaiian";
});
};
module.exports = findTastiestPizza;

CommonJS modules sychronously load dependencies, which is fine for Node.js but doesn't work very well in browsers because network requests are asynchronous.

To run our modular code in browsers, we need a module bundler like Webpack or Browserify to bundle up all of our dependencies into a single JavaScript file.

UIs aren't just JavaScript though. Our UI components can also depend on images, fonts and of course CSS. Webpack recognises that and, with the help of loaders, supercharges the require function so you can explicitly require all of your dependencies; not just the JavaScript ones:

require("stylesheets/modules/btn");
var img = require("images/default_avatar.png");

So back to our original problem: generating a single bundle.css programatically, based on the CSS your HTML actually needs, rather than manually maintaining a manifest.

All you need to do is explicitly require each view's own CSS or Sass dependencies like you would its JS dependencies, and Webpack will build a bundle that includes everything you need, performing any extra pre- or post-processing steps as required.

For example, given this entrypoint into our app...

// index.js
var React = require("react");
var Header = require("./header");
var Footer = require("./footer");
var Page = React.createClass({
render () {
return (
<div>
<Header />
<Footer />
</div>
);
}
});
React.render(<Page />, document.querySelector("#main"));

... which requires these components, which in turn require their own Sass dependencies...

// header.js
var React = require("react");
require("stylesheets/modules/header");
var Header = React.createClass({
render () {
return (
<div className="header">
Header
</div>
);
}
});
module.exports = Header;
// footer.js
var React = require("react");
require("stylesheets/modules/footer");
require("stylesheets/utilities/clearfix");
var Footer = React.createClass({
render () {
return (
<div className="footer u-clearfix">
Footer
</div>
);
}
});
module.exports = Footer;

... Webpack will generate a CSS bundle that looks something like this:

.header { /* ... */ }
.footer { /* ... */ }
.u-clearfix { /* ... */ }

Amazing!

Gotchas

Don't rely on source order

The main caveat to this approach is that you don't have any real control over the generated order of your CSS. Webpack will follow calls to require in the order that you specify them, appending to the generated CSS bundle as it goes.

So if we switched the order of our header & footer require calls...

// index.js
var Footer = require("footer");
var Header = require("header");

... the footer's dependencies, including the .u-clearfix utility, will be included first:

.footer { /* ... */ }
.u-clearfix { /* ... */ }
.header { /* ... */ }

If you're manually maintaining a manifest, it's common to include different types of classes in a specific order, e.g.

  • base
  • modules
  • utilities

... and rely on that order to ensure that styles further down the list will override any preceding styles.

When you can't rely on that manual ordering, you need to be more disciplined about how you use classes.

I don't like relying on source order anyway. I usually tend to avoid applying classes from different files to the same HTML element, especially if those classes both provide a rule for the same property. But occasionally you'll want to do something like this:

<div class="footer u-clearfix">

In that case, it becomes more important to explicitly increase the specificity of those selectors that you always want to apply. SUIT recommends liberally using !important for utilities, or you might want to try the duplicated class hack.

No global Sass context

If you're used to building your bundles using Sass @import, you're probably also used to a shared global context in Sass. You might do something like this...

@import "config/variables"
@import "mixins/vertical_align"
@import "modules/header"
@import "modules/footer"

... so those variables & mixins are available to any modules imported subsequently.

In the Webpack approach, each Sass file is compiled in isolation. This isn't a new idea, but I think it's a much better way of doing Sass. It just means you need to @import dependencies like variables & mixins wherever you use them:

// header.sass
@import "config/colors"
.header
color: $red
// footer.sass
@import "config/colors"
.footer
color: $blue

Small, isolated files with explicitly declared dependences are, in my opinion, a Very Good Thing.

Try it out!

I've created a simple example repo on GitHub that you can play around with.


Alternate versions of this post