Published on March 21, 2021

Configuring Tailwind CSS JIT in Angular

4 min read

Configuring Tailwind CSS JIT in Angular

This post is about getting the JIT compiler for Tailwind CSS to work with Angular.

Update

The JIT Compiler is now integrated directly into the TailwindCSS package ( PR ) under the option `mode`. You can set the mode to `jit` for compiling styles in runtime.

TLDR:

Since tailwindcss configuration is part of the Angular builder, we need to monkey-patch it for the time being. And in order for the jit compiler to watch changes to HTML, WATCH mode should be enabled.

Checkout ng-tailwindcss-jit for quick setup.

The Long Version:

Context

Tailwind CSS is a utility library for CSS that lets you style your HTML without writing any(mostly) CSS. It provides configurable pre-defined classes for all existing CSS properties.

Until now, when Tailwind CSS is used in a project, it leverages PostCSS to compile itself. And for production builds, purgeCSS is used to remove unused classes making the styles smaller.

But as the library grows and as the custom configuration increases, this generated file grows exponentially. This causes the development environment to be sluggish.

In order to tackle this, the Tailwind CSS team announced their own Just-in-time(JIT) compiler. Not only does this solve the problem with large files, it also makes the library more flexible to support many more usecases. See the announcement here

The problem

  1. The jit compiler works well with frameworks that have single-file components or where there is a direct reference to style files in template.

In the case of Angular, if there is a change to template(HTML) in development mode, only the corresponding module is reloaded.

This means that the style file is not recompiled since the PostCSS plugin isn’t triggered.

  1. Angular doesn’t allow you to configure custom PostCSS plugins unless you change the default devkit builder to @angular-devkit/build-webpack.

  2. From Angular v11.2 onwards, there is out-of-the-box support for Tailwind CSS. This is indeed a good thing. But for us to use the jit compiler, we now need to override this config or use a different naming for tailwind config. The default naming is now tied to default implementation.

Considerations

As I was thinking of how to solve this, I came up with a set of possible solutions.

  1. Change the default builder so I can specify my own PostCSS rule.
  2. Write a webpack plugin that can listen to file change events and override the file list to include the default style file. This means that I’m explicitly telling webpack to rerun PostCSS.
  3. Patch the default angular config, but will still need a way to trigger change for styles.

Limitations with Webpack

All the approaches required me to trigger PostCSS compilation. I decide to first try adding the jit plugin.

Changing the builder and updating the webpack config was straight forward. I was able to add the jit compiler plugin. But I soon understood that changing this means you are partially opting-out of the benefits of schematics. This was a real deal breaker for me.

I tried to listen to various hooks from webpack. This got me closer to understanding how Angular uses Webpack under the hook.

The goal was to update the dependencies array set with the path to default styles file (from angular.json builder options).

Webpack is confusing. Only because of how it has evolved over time. Some API’s don’t work, or they work only for newer versions.

Did you know? Even the latest version of Angular runs only on Webpack 4.4X.

Even after updating dependencies, nothing changed. That is because, the modified time of the file doesn’t change. And so the builder decides to use the existing bundle since nothing is modified.

Watcher in the JIT compiler

After a lot of trial and error, I wanted to know how this was achieved in other frameworks.

As I was looking at @tailwindcss-jit repo, I noticed the dependency on chokidar.

chokidar is a file watcher that handles the edge-cases of fs util. This made me curious to know if they did watch for file changes internally.

And Tada 🎉 I saw this.

...
if (
    env.TAILWIND_MODE === 'watch' ||
    (env.TAILWIND_MODE === undefined && env.NODE_ENV === 'development')
  ) {
    Promise.resolve(context.watcher ? context.watcher.close() : null).then(() => {
      context.watcher = chokidar.watch([...context.candidateFiles, ...context.configDependencies], {
        ignoreInitial: true,
      })
...

// https://github.com/tailwindlabs/tailwindcss-jit/blob/f9d8c892a0841e4633cee73ca8faac7cda06370e/src/lib/setupContext.js#L229

Fix

You should have guessed what I did by now 😄 .

Yes, I set the environment variable export TAILWIND_MODE="watch" and after overriding to use jit compiler plugin, everything worked. I can now update template and the CSS for that is recompiled.

For using the @tailwindcss/jit plugin, I decided to go with monkey-patching it in node_modules for two reasons:

  • This plugin will not exist once the compiler is a part of the original tailwindcss package. So nothing needs to be changed then.
  • I don’t want to break one of the best parts of using angular, the schematics.

ng-tailwindcss-jit

After testing it, I decide to write a small script for setting the compiler up.

https://www.npmjs.com/package/ng-tailwindcss-jit

You can add the jit compiler to your project by running the following command.

npx ng-tailwindcss-jit

What it does?

  • Set’s up a post install script to patch node_modules
  • Adds @tailwindcss/jit to devDepdendencies
  • Updates the start script to set the environment variable.

That’s all I have for you today. Hope you enjoyed reading it.

🎉 Interested in Frontend or Indie-hacking?

I talk about the latest in frontend, along with my experience in building various (Indie) side-projects