Tailwindcss and create-react-app, an alternative approach

Barnabás Molnár
7 min readJul 22, 2021

Introduction

Tailwindcss is an extremely popular utility-first CSS framework with a fantastic development and support staff behind it. If you’re reading this article, you probably already know this, so we’re not here to extol the virtues of tailwind. Instead, we’re here to talk about how to integrate it with yet another insanely popular and widespread tool: create-react-app (CRA).

I’ve included a TL;DR section near the end if you just want to get going really quickly. If you’re interested in the why and how of this approach, then I’ve done my best to give you a comprehensive walk-through below.

Motivation

First of all, why would one want to use CRA to set-up a build and dev configuration for React apps? While there are now several alternatives — be that Parcel, Next.js, Vite, Snowpack, just to name a few — that hasn’t always been the case. You could create your very own configuration from scratch, by hand as well, of course. I have a suspicion, however, that most of us don’t particularly enjoy setting up complex build and dev configurations and instead opt for a tool like the ones mentioned above. It just so happens that CRA is, with nearly 90k GitHub stars at the time of writing, quite possibly the most popular tool out there for this purpose. So, if you’re on CRA and can’t or won’t consider alternatives for whatever reason — but at the same time still want to take advantage of tailwindcss to its fullest — then this article is definitely for you.

Now, you might be thinking: but there are already ways to integrate the two tools; there’s even a how-to on tailwind’s website. Indeed, and if you scour the web, you’ll find many other hand-rolled solutions. I’m here to tell you that none of these options are optimal.

The issue is two-fold. Tailwind is a PostCSS plugin and requires PostCSS version 8 to take advantage of its latest features. CRA, meanwhile, is still on PostCSS v7, and it also doesn’t expose an API that would let you customise its PostCSS config.

To combat the latter issue, the official guide from the tailwind guys makes use of CRACO, a configuration layer for create-react-app that lets you customise CRA without ejecting. This sounds exactly like what we need, except CRA does not support PostCSS 8, which is a requirement to make use of tailwind’s excellent JIT compiler, introduced in v2.1. If you don’t particularly care about that, then the CRACO + compat build option may work out well for you. For me, however, JIT mode is an absolute must with all the goodies that it brings with itself.

Another option that you’re likely to find elsewhere is to set-up a completely separate tooling that would run concurrent to the CRA dev environment. Dumbed-down, with some of the details stripped away, it’d look a little something like this:

  • use postcss-cli to take a source CSS
  • run all the postcss plugins on said source CSS
  • spit out a dist CSS
  • include dist CSS in the react app

It does work, in my experience; in fact, this is something I’ve been doing for quite some time myself. I never particularly liked it, though, because it’s yet another separate layer of tooling with very loose integration into my main workflow. It also adds a slight overhead when building your project in that the CSS is compiled twice: once by my external tooling, once by CRA’s own build step. Not great. What now?

The solution

Let’s recap very quickly what the issue is:

  • CRA is on PostCSS 7 and we need 8
  • CRA won’t let you customise its underlying PostCSS config

This is what we’ll have to overcome. And we’ll want to do so in such a way that at the end we have a nice integrated solution that we won’t have to constantly fiddle with.

What we need is a… fork. That’s right. Forking is a lesser known, albeit extremely powerful alternative to ejecting. We’ll want to fork react-scripts, make some changes and then update our app to use our very own version of react-scripts. If this sounds scary, insane or a combination of the two… it really isn’t. We’ll discuss why it’s not that bad in the next section, but for now, just roll with it.

We kinda know what we have to do already.

  • We have to update the PostCSS dependencies to their respective latest versions. This sounds simple enough.
  • We also have to gain access to the PostCSS config and make the necessary modifications so as to integrate it with tailwind properly. This, admittedly, sounds a bit trickier.

Here’s a thought. Instead of hard-coding tailwind into the PostCSS config, we could make it so that the consumer (=our app basically) can supply its very own PostCSS config with a postcss.config.js file. That way, we have a lot of flexibility and can provide custom, bespoke PostCSS configs for each CRA installation we do in the future.

Now is the time for some honesty. I’m obviously not the first one to have this thought. In fact, a customisable PostCSS config is one of the most often requested features for CRA. Pull requests implementing this functionality have also been made. Here is one, for instance: https://github.com/facebook/create-react-app/pull/8907. If you actually look at the commit, you’ll see that there’s no magic in there at all. All this modification does is check if a custom postcss config has been supplied by the consumer; if so, then that config will be used, if not, CRA’s own preset config will be used. Let’s clean this code up a little and have a look at the result.

Below, you will find two links to commits that achieve what we set out to do in the beginning of this section:

Here, we updated the PostCSS dependencies. Note that not all of them could be upgraded to their absolute latest versions. postcss-loader has a v5 but that is only compatible with Webpack v5 and CRA is on v4 for now.

Here, we added support for supplying custom postcss configs with postcss-load-config. This is essentially the same code from the PR above, just slightly cleaned up.

And with that, most of the work is done.

What’s left now is to package all this up and publish it to NPM. In theory, you should be able to test it locally like so:

npx create-react-app my-app --scripts-version file:../path-to-forked-create-react-app/packages/react-scripts

however, for some reason, it spits out an error saying that the react-scripts version is not compatible with templates. Template here refers to this: https://github.com/facebook/create-react-app/tree/main/packages/cra-template. This is definitely a bug with create-react-app though, because once this package is in a remote npm registry, it works just fine. 🤷‍♂

I’ve done the legwork here and released a package to npm so that you can see for yourself:

npx create-react-app my-app --scripts-version @barna90/react-scripts

And there you have it. You can now follow the tailwind documentation, install tailwindcss, add a postcss.config.js, add a tailwind config maybe and you’re golden. 🎉

Caveats

What we’ve done above works pretty great… for now. Since we’re using a fork of create-react-app and we’re packaging our own react-scripts library, it is now on us to maintain it and keep it up to date. Updating to a new version is no longer as simple as

npm install --save --save-exact react-scripts@version-number

On the other hand, it’s also not as bad as you might first think. It’s fairly easy to sync a fork of a repository to keep it up-to-date with the upstream repository. Also, CRA tends to be rather stable and they don’t release new versions that often.

I think this is a decent trade-off in the end: the occasional maintenance burden, for me at least, is worth the nice and tight integration between these tools.

One pretty important thing I’ve neglected to mention so far is that if you want to do the whole forking business yourself, make sure to branch off from a stable branch. Note that main is not stable. I suggest taking the latest stable tag (v4.0.3 at the time of writing) and branching off of that.

TL;DR

To get latest tailwind with all its bells and whistles to play nice together with create-react-app in a tightly integrated solution, either

Use my NPM package like so to scaffold a react app:

npx create-react-app my-app --scripts-version @barna90/react-scripts

then install and set up tailwind as usual following their documentation.

Make your own:

If you’re interested in the why and how, you’ll find it in the sections above.

Closing thoughts

In this article, we’ve explored how to closely integrate tailwind and CRA, weighing all the advantages and disadvantages of this particular approach against each other. In fact, this solution isn’t strictly only for tailwind per se, as we’ve added the ability to enhance CRA builds with customised postcss configurations. In a future article, we might even create a custom CRA template that has tailwind and some of its official plugins prepackaged already. Stay tuned! For now, please let me know if you have any feedback. Cheers.

--

--

Barnabás Molnár

Full-stack JavaScript engineer with a keen interest in Functional Programming. Haskell trainee.