Add Tailwind to Create React App without ejecting

05 May 2020

Tailwind CSS is a highly customizable, low-level CSS framework that gives you all of the building blocks you need to build bespoke designs without any annoying opinionated styles you have to fight to override.

Tailwind CSS has completely changed the way I develop web apps thanks to the utility first workflow. And with version 1.4 now including PurgeCSS as a built-in process it has never been easier to get a clean and lean build for your next project.

Best of all, you don't need to eject a Create React App to get the full benefits of adding Tailwind to your project. In fact, it's as simple as installing Tailwind CSS, creating a PostCSS config and making a few tweaks to your npm scripts.

Create the React app

We'll start with a default Create React App to keep things simple.

npx create-react-app cra-tailwind && cd cra-tailwind

Default project clean-up

Now that we've created our default React app lets quickly get some housekeeping out of the way before we dive into the real topic of this article.

Since all of our CSS ends up in a single file we can delete the provided CSS files.

rm src/index.css src/App.css

The src/index.js entrypoint references index.css. Open that file up and remove the import.

src/index.js
// Remove this line
import './index.css';

In a similar way, src/App.js is still importing App.css. Remove this reference.

src/App.js
// Remove this line...
import './App.css';

Now that we have that out of the way its time for the fun stuff.

Add Tailwind to the project

Install the required dependencies

Install the development dependencies from our project root.

npm install --save-dev tailwindcss postcss-cli autoprefixer

Briefly, tailwindcss is the framework itself, postcss-cli is used to build our generated CSS, and autoprefixer is used to add all required vendor prefixes in our final built CSS.

Create the Tailwind CSS

To keep things clean, I prefer to have a dedicated styles directory containing a tailwind.css file. It is also the directory within which Tailwind will generate the final build.

mkdir -p src/styles && touch src/styles/tailwind.css

Now add the @tailwind directives for base, components and utilities to src/styles/tailwind.css.

src/styles/tailwind.css
@tailwind base;
/* Your own custom base styles */

@tailwind components;
/* Your own custom component styles */

@tailwind utilities;
/* Your own custom utilities */

This tailwind.css file constitutes our project's pre-generated CSS and is where we will also write any of our extra custom CSS.

Generate the Tailwind configuration

./node_modules/.bin/tailwindcss init

This will create an empty tailwind.config.js file in your project root that can be used to dramatically alter the final build.

A brief word about Tailwind customisation

Running the Tailwind build process will effectively replace @tailwind base; with all of the framework's base classes and do the same for components and utilities. This generated output will be saved in a separate file. But the build process can do way more than just inject those styles if you take the time to dive into the customisation options.

The truly powerful feature of this framework is the ability to fully customise the variables of the build which can dramatically change the generated CSS to meet your exact needs. Do you need new default breakpoints? Add it to the config and Tailwind will automatically generate all related classes. Need to completely override some of the colours in your build? Add it to the config and Tailwind will automatically generate all related classes.

I cannot emphasise enough just how powerful the customisation options are and the massive amount of time it can save you. So please do spend time reading up on how to customise Tailwind.

Create the PostCSS config

Start by creating the postcss.config.js configuration file in the project root.

touch postcss.config.js

Populate it with

postcss.config.js
module.exports = {
  plugins: [require('tailwindcss'), require('autoprefixer')],
};

When we run PostCSS through our build scripts it will first send our custom CSS (/src/styles/tailwind.css) through the Tailwind CLI to generate all of the classes required after applying any configuration customisations.

Once completed, PostCSS will then proceed to run autoprefixer on the output to automatically add any vendor prefixes to our CSS based upon our browserlist. Tailwind doesn't come with any vendor prefixes and this is one of the main reason we use PostCSS to generate our Tailwind build instead of relying solely on the Tailwind CLI.

Create Build Scripts

During a build, Create React App doesn't recognise the postcss.config.js from our project root so we have to change our npm scripts to cater for this.

Install dependency for cross-platform environment variables

This is potentially optional based on your use case but I'm including it here to be kind to Windows users. The cross-env package allows us to set environment variables in our npm scripts that works across platforms.

npm install --save-dev cross-env

Option 1: Generate once at start of build process

The best part about using Tailwind is that you'll likely not write any actual CSS once you have it setup in your project. You apply the already generated Tailwind classes to your HTML and JSX. So there's little reason to watch the CSS files and instead rely on Create React App watching the JS, JSX and other relevant files during development and reloading on those changes only.

Inside package.json change the scripts to include the following.

package.json
"scripts": {
  "build:css": "postcss src/styles/tailwind.css -o src/styles/main.css",
  "prestart": "npm run build:css",
  "start": "cross-env BROWSER=none react-scripts start",
  "prebuild": "cross-env NODE_ENV=production npm run build:css",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
},

The build:css script runs PostCSS against src/styles/tailwind.css and applies any plugins we have setup in postcss.config.js. The output is then saved in src/styles/main.css. It is this generated file that we import into our application.

We are taking advantage of npm's pre hooks to kick off a sequential build of the Tailwind generated CSS.

  • Production Build: If we run npm run build it will first run the command(s) from prebuild. Notice that we're setting the NODE_ENV variable on the prebuild to ensure Tailwind runs production optimisations.
  • Development Build: When developing locally we run npm run start which will first run prestart. We do not set the environment to production here as we want access to the entire Tailwind framework. Furthermore, I don't like that Create React App automatically opens my default browser so I usually set BROWSER=none ahead of running react-scripts start. I run Choosy and when the development server is ready I simply cmd-click on the link to select the browser within which I wish to test the site.

Option 2: Watch CSS files for changes (do not open default browser)

We first need to install npm-run-all to provide a cross-platform method of running multiple npm scripts in parallel. Windows, for example, does not support & when trying to chain npm run commands together.

npm install --save-dev npm-run-all

We create a specific watch:css command that will constantly watch for any changes to the tailwind.css file and regenerate the main.css file upon save. These will in turn trigger Create React App to refresh our browser to show the new changes. It's a convenient workflow and works well but Tailwind releases us from having to do too many changes to the actual CSS so having a watcher running feels a bit wasteful.

package.json
"scripts": {
  "start:react": "cross-env BROWSER=none react-scripts start",
  "start": "npm-run-all -p watch:css start:react",
  "watch:css": "postcss -w src/styles/tailwind.css -o src/styles/main.css",
  "build:css": "postcss src/styles/tailwind.css -o src/styles/main.css",
  "prebuild": "cross-env NODE_ENV=production npm run build:css",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
},

As with Option 1, I have it set so that the default browser does not automatically open when running the development server.

Option 3: Watch CSS files for changes (automatically open default browser)

There is another reason I don't like the default browser to automatically open on a development build. It relates to a race condition if Tailwind hasn't completed compiling the css ahead of the page loading. The page will open but it will be blank and React will not load forcing you to have to manually refresh the page.

If you wish to still maintain the automatic opening of your default browser when developing the site you will either need to refresh the page after the browser loads or add a short delay ahead of react-scripts start.

package.json
"scripts": {
  "start:react": "sleep 3 && react-scripts start",
  "start": "npm-run-all -p watch:css start:react",
  "watch:css": "postcss -w src/styles/tailwind.css -o src/styles/main.css",
  "build:css": "postcss src/styles/tailwind.css -o src/styles/main.css",
  "prebuild": "cross-env NODE_ENV=production npm run build:css",
  "build": "react-scripts build",
  "test": "react-scripts test",
  "eject": "react-scripts eject"
},

You'll notice the only change is to the start:react script where we have added a sleep 3 delay ahead of running react-scripts start and removed the BROWSER=none environment variable. Unlike trying to run npm scripts in parallel, && is supported on Windows so no need to create a cascade of scripts for our delay.

Where to import the generated CSS

You only need to import the generated file once. I usually import it into src/App.js in a Create React App but you could also import it into index.js or wherever makes sense within your own app. Be sure that it is high enough in the cascade to ensure the styles are available when adding Tailwind classes to your JSX components.

Test the build

I've tried to replicate the default Create React App homepage using Tailwind classes with a default configuration.

Replace src/App.js with the below code.

src/App.js
import React from 'react';
import logo from './logo.svg';
import './styles/main.css';

function App() {
  return (
    <div className="text-center">
      <header className="bg-gray-900 min-h-screen flex flex-col items-center justify-center text-4xl text-white">
        <img
          src={logo}
          className="App-logo h-64 pointer-events-none"
          alt="logo"
        />
        <p>
          Edit <code>src/App.js</code> and save to reload.
        </p>
        <a
          className="text-blue-300"
          href="https://reactjs.org"
          target="_blank"
          rel="noopener noreferrer"
        >
          Learn React
        </a>
      </header>
    </div>
  );
}

export default App;

We'll also want to recreate a few of the custom styles by using @apply for Tailwind classes or old-school CSS for the logo animation.

Replace src/styles/tailwind.css with the code below.

src/styles/tailwind.css
@tailwind base;
/* Your own custom base styles */
body {
  @apply antialiased;
}

@tailwind components;
/* Your own custom component styles */
@media (prefers-reduced-motion: no-preference) {
  .App-logo {
    animation: App-logo-spin infinite 20s linear;
  }
}

@keyframes App-logo-spin {
  from {
    transform: rotate(0deg);
  }
  to {
    transform: rotate(360deg);
  }
}

@tailwind utilities;
/* Your own custom utilities */

We can now start the development server to view our placeholder app.

npm run start

Once that is ready, open src/styles/main.css and look at the styles that Tailwind has generated. That CSS file is absolutely massive with almost 70,000 lines!

The development build of Tailwind is large by design ensuring all of the framework's classes are available to you while you build the site. You will only ever use a small percentage of these and Tailwind provides a convenient way to trim that down for your production builds.

Optimise the production build

Update the Tailwind configuration.

The setup for running PurgeCSS used to be a little more involved requiring installing dependencies and adding a regex pattern to our PostCSS configuration. Thankfully this is now a built-in feature as of version 1.4.0 and requires nothing more than defining the file patterns that may contain our Tailwind classes.

Replace the contents of the tailwind.config.js file in your project's root.

tailwind.config.js
module.exports = {
  purge: ['./src/**/*.html', './src/**/*.js', './src/**/*.jsx'],
  theme: {
    extend: {},
  },
  variants: {},
  plugins: [],
};

Now that we have the purge settings in place, Tailwind will look through those files and treeshake any unused styles from the final production build. It was for this reason that we explicitly set NODE_ENV=production in our prebuild stage.

If you're curious, read more about how Tailwind controls the file size for production builds.

Test the Production Build

If you're still running the development server hit Ctrl+C to shut it down.

We're now ready to run a production build.

npm run build

Once that completes if you open the newly generated src/styles/main.css file you should find it has shrunk down to a file containing only the styles we have thus far used in our project.

Add the generated CSS file to gitignore

Be sure to add the generated CSS file (src/styles/main.css in our example above) to your .gitignore. The generated file's contents completely depending on whether we are running a production or development build.

It might feel weird leaving a required file out of version control given we are importing the generated file into our App.js. This isn't an issue at all since we always generate the file ahead of the production build or running the development server.

Conclusion

Tailwind CSS truly has had a huge impact on improving my workflow when developing web apps. I encourage anybody who is still skeptical of the syntax and adding so many classes to your HTML and JSX to just give it a try. The framework keeps going from strength to strength and I cannot imagine starting a project without first reaching for Tailwind.

Share this article
Was this post helpful?
Buy me a coffeeBuy me a coffee