A Better Approach for Using Purgecss with Tailwind

Greg Kohn, Former Senior Front-End Developer

Article Category: #Code

Posted on

Purgecss is an indispensable frontend tool, especially when used alongside TailwindCSS. Its mission and logic are simple -- so simple, that you need to help it if your classes do not appear verbatim in the code. Here are some approaches.

A number of us at Viget have been using TailwindCSS recently, and -- at least for the converted -- it’s been simply awesome. We’ve already written about some lessons learned, and this is another take on improving your Tailwind workflow.

If you’ve already decided to disavow years of naming classes semantically and give Tailwind and utility-first CSS a chance, you may have heard of Purgecss. Another process for your build tools, Purgecss solves a big problem in a really straightforward way: Eliminating unused CSS classes through predictable regex matching. After comparing your markup files with your CSS, it weeds out any unused classes for a smaller file.

While not a Tailwind-specific tool, there’s a good reason Tailwind’s docs specifically mention how to configure Purgecss. Tailwind, by intention, is aiming to equip you with an arsenal of utility classes by generating more than you need. There are some best practices which will help keep this overall build size down, like limiting your colors and breakpoints or turning off the modules by default before adding them as necessary. Still, you’ll inevitably generate classes that go unused. And honestly, approaching your configuration with an unrelenting miserly attitude will slow you down and make development less fun. By leaning on Purgecss, there’s no worry that the CSS your users download will only include classes that are ultimately needed.

Well… almost. There are still some gotchas that will crop up, so let’s dive into what these look like and some potential solutions.

Hey, I need that!

There is beauty in the simplicity with which Purgecss thinks, but the CSS classes you need don’t necessarily show up verbatim in your markup. Without any further configuring, they'll be nixed. Consider:

  • Dynamic classes. You've pieced the class together with variables.
{% set width = 'sm:w-1/' ~ maxPerRow %}
  • Third-party classes. You’re styling classes that are injected onto the page via JavaScript, perhaps by a dependency or embedded form.
  • Abstracted HTML tags. Classes, of course, aren’t the only way to select
    an element. Some frameworks by default abstract out HTML tags you might be targeting in a reset, such as Next.js. In another case, you might be styling tags that are generated by the WYSIWYG editor in your CMS which otherwise do not exist in your codebase (I’ve been bitten by ol before).

Often, you can rely on the built-in whitelisting techniques that Purgecss offers. These are three-fold:

  1. Add specific selectors to your config file
  2. Use a regular expression in your config file
  3. Add special comments in your CSS

The first is straightforward but probably not the best or most maintainable choice, as we’ll see. Regular expressions are powerful but also have some downsides, leaving CSS comments as the more reliable approach for most situations. Still, none of these three methods establish a maintainable workflow for dynamic classes, so we’ll need to come up with something different there. Let’s look at these points more closely.

Whitelisting with Specific Selectors and Regular Expressions

Targeting specific selectors or using regular expressions are essentially the same idea, with the latter more robust. In both cases, you are opening your build tool config and specifying something which, if matched, will prevent classes from being removed. And in fact, third-party classes are typically great candidates for being handled by regular expressions, as they are usually namespaced and thus easily targeted.

For example, I recently styled an embedded Marketo form which ships with a ton of classes that needed tweaking (well, a complete makeover). Tailwind’s @apply directive made that part easy, but I still had to tell Purgecss to keep my changes as those classes didn’t appear anywhere in the markup. Marketo namespaces their classes by prepending mkto to each one, so we could solve the problem by adding a single line to the Purgecss config:

purgecss({
  // ... other settings
  whitelistPatterns: [/mkto/]
})

This is a powerful and concise solution, and I wouldn’t begrudge you for using it. However, I can’t help but feel bothered by how this divorces the logic behind our decision to whitelist this from the actual code we are whitelisting. That train of thought is easy enough to follow here, but if we needed to whitelist something more vaguely named for use with a dependency, it would not be as easy to track down this relationship.

For instance, Micromodal — an awesome modal library — forces little CSS on you but does expect to toggle the modal visibility via an is-open class that it adds directly on the DOM. Again, we could target this in our config:

purgecss({
  // ... other settings
  whitelist: ['is-open']
})

But the link between this and Micromodal is heavily obscured. Even if you did know it was connected, this setting is globally applied, so it’d be hard to know with absolute certainty you could remove it if you were swapping that modal library for something else.

Admittedly, the earth won’t stop spinning if a few unused classes smuggle themselves into the final build. But if I have a chance to make my code a bit more obvious, I’ll take it. And that brings us to comments.

Whitelist with CSS Comments

Using CSS comments may not be as terse as regex, but it is certainly a more declarative solution.

To return to the Marketo example, we would achieve whitelisting by simply wrapping my file of horrific overrides in two comments:

/* purgecss start ignore */
.mktoLabel {
  @apply text-blue;
}

.mktoButton {
  @apply rounded;
}

/* ...more rules */

/* purgecss end ignore */

With this approach, we can precisely convey the intent as well as directly tie our whitelist logic (if you can still call it that) to our code. Now removing everything wholesale is perfectly safe and there is no growing centralized list of whitelist patterns to maintain.

Take note that Purgecss needs to see these comments, so ideally that task comes before you minify and strip out comments in your build tools. If, for some reason, those are in the opposite order and you can’t change it, you’ll need to use whatever “special” comment syntax that your minifier ignores, which typically means adding an exclamation point like so:

/*! purgecss start ignore */

Currently, Purgecss leaves these in after parsing so your final build will include them if you go this route.

A Workflow for Dynamic Classes

The built-in methods for handling whitelisting are great, but they leave a big gap when it comes to dynamic classes. This is especially obvious if you are composing classes generated by Tailwind, as there is no CSS file to use comments within. Sure, you could start filling your Purgecss config with classes, but that invites the same maintainability problems described above. Adam Wathan, Tailwind's creator, puts forth a terrific technique he dubs "writing purgeable HTML". By this, he means to key off variables to choose a complete class name, as opposed to constructing a class name. For example:

{# In this example, maxPerRow can be 2 or 3 #}
{% set width = maxPerRow == 2 ? 'sm:w-1/2' : 'sm:w-1/3' %}

However, if you are using a variable with a wide variety of possible values, this would mean writing out long, verbose conditionals. In that case, I've found it simplest to create a commented-out list of the possible values for your dynamic classes inside the relevant file. Because Purgecss relies on such a straightforward matching strategy, the classes just need to appear — even if they’re inside of a template language comment. For clarity, I begin the comment with purgecss as a convention.

{# In this example, maxPerRow can be a number 2 through 6 #}
{% set width = 'sm:w-1/' ~ maxPerRow %}
{# purgecss: sm:w-1/2 sm:w-1/3 sm:w-1/4 sm:w-1/5 sm:w-1/6 #}

Underwhelmed? This approach might be brain-numbingly simple, but it offers a lot of familiar benefits: you end up with intuitive, maintainable lists of whitelisted classes instead of a centralized, untouchable mess.

By including them in your markup, your whitelisted classes are essentially scoped to the appropriate chunk of code. They are still globally whitelisted, sure, but — like the CSS comments — you know exactly what code they are associated with, so there’s no fear in removing them should you delete the code composing those class names. For this reason, you should always include the full list of options, even if items are repeated elsewhere; siloing each keeps things predictable.

Finally, be sure to add this convention to your project’s README and even highlight some of the issues covered here when using Purgecss, as it can be quite a perplexing problem to track down if unfamiliar.

Recap

Purgecss is an indispensable frontend tool, especially when used alongside a utility class generator like Tailwind. Its mission and logic are simple -- so simple, that you need to help it if your classes do not appear verbatim in the code. Whitelisting via strings and regex patterns can be a powerful way to do so, but if using the special CSS commenting syntax is possible, it is the most maintainable solution. For dynamic classes, try to avoid concatenating the name; if this is unwieldy, inline all the possible values beneath your logic in a comment, which keeps these whitelisted classes associated with the relevant code.

Tailwind's 1.0.0 release is imminent -- there's never been a better time to try utility classes with Purgecss!

Related Articles