I’ve been using Tailwind CSS for some of my Rails apps with great delight over the last few months. One downside of utility-first CSS frameworks, however, is the fact that they ship with a myriad of potentially useful classes of which your HTML might use but a few percent.

PurgeCSS (which is available as a PostCSS plugin) to the rescue! To simply quote the docs, it

analyzes your content and your css files. Then it matches the selectors used in your files with the one in your content files. It removes unused selectors from your css, resulting in smaller css files.

Install PurgeCSS

Okay, without further ado, let’s add it to the javascript package of our (webpacker enabled) Rails app:

$ bin/yarn add @fullhuman/postcss-purgecss

Since you probably do not want to enable it in your development environment - it does take some time to go through all your code and strip out the unused selectors every time - you’ll probably configure your postcss.config.js like this:

if (
  process.env.RAILS_ENV === "production"
) {
  environment.plugins.push(
    require("@fullhuman/postcss-purgecss")({
      content: [
        "./app/views/**/*.html.slim",
        "./app/helpers/**/*.rb"
      ],
      defaultExtractor: content => content.match(/[A-Za-z0-9-_:./]+/g) || [],
      whitelist: collectWhitelist(),
      whitelistPatterns: [],
      whitelistPatternsChildren: [/trix/, /attachment/]
    })
  );
}

module.exports = environment;

Only, that doesn’t quite lead to the expected results with the Slim templating language. Slim is indentation-based and very terse, you basically write out CSS selectors in a tree:

.flex.justify-end.items-center
   .flex-1.text-base 
   .flex-shrink.text-sm
   / ...

What’s more, Tailwind uses a lot of, let’s say unusual characters inside its CSS classes, such as /, :, and even .. Working with those in Slim is hard enough (and one reason I switched back to ERB for most Tailwind-based projects, but sometimes you just don’t have that choice), but getting the correct extractor regex pattern is some orders of magnitude harder.

Convert It Back To ERB

So, how can we help that? I decided to go with the naivest approach there possibly is, namely, converting Slim templates back to ERB before the webpacker assets are compiled. I was hesitant to try this until I realised slim offers a dedicated erb_converter for these purposes. So I wrote a custom script, and placed it in lib/scripts/slim_erb.rb:

require "slim/erb_converter"

open(Rails.root.join("tmp/compiled.html.erb"), "a+") do |compiled|
  Dir.glob(Rails.root.join("app/views/**/*.html.slim")).each do |slim_template|
    open slim_template do |f|
      slim_code = f.read
      erb_code = Slim::ERBConverter.new.call(slim_code)
      compiled.puts erb_code
    end
  end
end

This will concatenate all your Slim files into one large ERB file placed in your app’s tmp folder - which, since we only care about the selectors used in there - is just good enough. What remains to do, is to swap the content key in the PurgeCSS config:

"./app/views/**/*.html.slim" => "./tmp/*.html.erb"

In your deployment script, you just have to make sure you invoke this script as a Rails runner script (and optionally delete the temporary file):

$ bundle exec rails r lib/scripts/slim_erb.rb && bundle exec rails webpacker:compile

Fingers crossed 🤞🏻, your purged CSS should now contain all the selectors present in your view templates. Of course, the usual caveats about whitelisting third party libraries (like trix, for example), still apply.

Post Scriptum

I can foresee some objections as to how Tailwind allows for all types of sophisticated configurations (and might already have PurgeCSS included, depending on your version). I used Tailwind mainly as a stand-in for any kind of utility-first CSS framework. I’ve used this technique successfully with Semantic UI, Bootstrap, and others.