PurgeCSS Logo

We leverage CSS frameworks like Bootstrap or Tailwind in our applications, but we likely won't use all elements from the framework. This means that we’re serving larger CSS files than we need to. Wouldn’t it be great if there was a way only to include the CSS that we’re actually using? Well, there is! Enter PurgeCSS.

This post is somewhat a continuation of my previous post How to minify and add vendor prefixes in Rails 7. You can use these two posts together to achieve some great CSS processing.

What’s going to happen

We will use a tool called PurgeCSS to weed out all the CSS selectors we aren’t using. I’ll be using a Rails app with Bootstrap as the primary example, but the concept should apply to most setups.

Here’s the gist:

  1. Specify our application CSS for PurgeCSS.
  2. Explain to PurgeCSS where our HTML lives.
  3. PurgeCSS then scans our HTML (partials, layouts, views, etc.) for the selectors we’re using and only includes those in the final CSS output.

When do we perform the processing?

There’s probably not a great reason to purge unused CSS in development. Instead, we’ll make it part of the final production build process. We’ll do this by adding a build script in package.json that will allow us to execute the task in various ways. Setting it up like this has the added benefit of allowing you to purge things locally if you just want to goof around and see how things are working.

Let’s get started

Start by creating a new Rails app that includes Bootstrap with:

rails new purge-example --css=bootstrap -d=postgresql

Now, scaffold out some pages so we have something to look at:

rails generate scaffold page

Set up your database or you’ll get an error:

rails db:setup
rails db:migrate

Finally, let’s define our root route in config/routes.rb:

Rails.application.routes.draw do
  resources :pages
  # Define your application routes per the DSL in https://guides.rubyonrails.org/routing.html

  # Defines the root path route ("/")
  root "pages#index"

Add a single Bootstrap element

Let’s add a simple Bootstrap button to illustrate how things work. Add the following to app/views/pages/index.html.erb:

<a href="https://getbootstrap.com" class="btn btn-primary my-4">Get Bootstrap</a>

We include three CSS selectors from Bootstrap: btn, btn-primary, and my-4. If we successfully set things up correctly, we should only see these three selectors included in our final CSS output. Let’s peek at what our CSS looks like at this stage.

You can find the compiled CSS in app/assets/builds/application.css or you can view it in your browser’s dev tools, whichever you prefer. Either way, you should be staring at a lot of CSS. My file shows over 18K lines. Wow! We don’t need all that. Our contrived example app is only displaying a single button.

Add PurgeCSS

You’re welcome to follow the instructions over at PurgeCSS, but I’ll walk you through it here. We’re first going to use Yarn to add the package:

yarn add @fullhuman/postcss-purgecss --dev


Next, you need to create purgecss.config.js in the root of your app. This file will contain all our configurations. Here’s an example you could use as an initial starting point:

// https://purgecss.com/configuration.html

module.exports = {
    content: ['./app/**/*.html.erb', './app/helpers/**/*.rb'],
    css: ['./app/assets/builds/application.css'],
    output: './app/assets/builds',
    safelist: {
        standard: [
        deep: [/some-custom-class/]
  • content: PurgeCSS needs to analyze your HTML to see what selectors you’re using. Make sure to specify the files or globs to anything with HTML markup.
  • css: This will likely be the css file Rails has already processed (via esbuild).
  • output: The directory you’d like the purged CSS file written to. In this example, it will overwrite the existing application.css.
  • safelist: This setting is important because there are times you may have HTML being generated dynamically. In this case, PurgeCSS won’t know what CSS selectors you’re using. Additionally, this setting is useful for any “must-have” selectors.
  • deep: If you have nested CSS and don’t want to bother listing out all child selectors, you can provide a regex pattern to have everything included.

Check if things are working

At this point you’re free to test things out by running:

npx purgecss --config ./purgecss.config.js

Assuming the command successfully executes, you can verify everything has worked by going back to app/assets/builds/application.css and checking on how many lines you now have. In my case, I went from over 18K to just over 400. That’s a helluva reduction!

Get ready for deployment

We need to add a build script along with a Rakefile task so purging can happen when Rails precompiles all our assets for our production environment.

Open up package.json and add a new build script called "build:purgecss":

"scripts": {
  "build": "esbuild app/javascript/*.* --bundle --sourcemap --outdir=app/assets/builds --public-path=assets",
  "build:css": "sass ./app/assets/stylesheets/application.bootstrap.scss:./app/assets/builds/application.css --no-source-map --load-path=node_modules",
  "build:purgecss": "npx purgecss --config ./purgecss.config.js"

Now open up Rakefile and add a task that will be executed during precompilation:

# https://github.com/rails/cssbundling-rails/blob/main/lib/tasks/cssbundling/build.rake
# This helps the additional build scripts in package.json to run. E.g. build:postcss
namespace :css do
  desc "Build for CSS"
  task :build do
    unless system "yarn install && yarn build:purgecss"
      raise "cssbundling-rails: Command css:build failed, ensure yarn is installed and `yarn build:purgecss` runs without errors"

All done!

Congratulations, you now have a leaner CSS application file! PurgeCSS won’t run in development (unless you tell it to), but it will trim things down during the build process in production. Way to save some KBs 🙂.

I have one final note. Please document the existence of this tool for you and your team. Without a bit of care, you could end up with some funky results in production due to missing CSS selectors. There is certainly a tradeoff between optimized file size and some extra due diligence. It’ll be up to you to determine how to best fit it into your existing pipeline. That being said, the results are fantastic if they can work in your existing setup.

Written by Matt Haliski
The First of His Name, Consumer of Tacos, Operator of Computers, Mower of Grass, Father of the Unsleeper, King of Bad Function Names, Feeder of AI Overlords.