You've probably heard of Tailwind by now, but if you haven't, it is essentially a utility-first CSS framework and is much less opinionated compared to other CSS frameworks like Bootstrap and Foundation.
According to tailwindcss.com, it is described as:
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.
In my opinion, this makes Tailwind a great option to use for your projects. You get the benefits of rapid development, without the opinionated styles that you ultimately override anyway.
So what if we want to use Tailwind with React, is it as simple as just including a style sheet? Not exactly. Because of how Tailwind is built and some of the optimizations they have in place (i.e. purging unused CSS), there are a couple of extra steps to properly set it up in a React project.
To get started, either jump into your own React project or start up a new one with
npx create-react-app tailwind-react-app
.Installing Dependencies
Next, let's install a couple of dependencies via
npm i @fullhuman/postcss-purgecss postcss-cli tailwindcss --save-dev
.A brief explanation of each dependency:
fullhuman/postcss-purgecss
- used to purge unused CSS to create the smallest final CSS file possible. If you don't purge unused CSS, tailwind is actually over 2MB uncompressed.postcss-cli
- Tailwind is a PostCSS plugin. You don't need to use PostCSS as the preprocessor, but in this case we will in order to keep things simple.tailwindcss
- the Tailwind library and utilitiesNow that we have our dependencies installed, we need to create a couple of config files.
First, create your Tailwind config by running
npx tailwind init
in the project root. You can choose to update some of the Tailwind config here if you wish.Next, create your PostCSS config by creating a
postcss.config.js
file in the project root and adding the following content:const purgecss = require('@fullhuman/postcss-purgecss')({
content: ['./src/**/*.js', './src/**/*.jsx', './public/**/*.html'],
defaultExtractor: (content) => content.match(/[A-Za-z0-9-_:/]+/g) || [],
});
module.exports = {
plugins: [
require('tailwindcss'),
...(process.env.NODE_ENV === 'production' ? [purgecss] : []),
],
};
In the first portion, we require and setup PurgeCSS. This requires two pieces. The first is the list of purgeable files which are js, jsx, and html (you can also add ts and tsx if you use TypeScript). The HTML is required so the base styles (e.g. body, html, etc) don't get purged.
The second portion is used to define the extractor function (i.e. rules for which content to keep). PurgeCSS is naive by nature, meaning it will look for content that matches that Regex in the file types listed above. This is important to know and will be revisited when we discuss patterns below.
Now that we have our dependencies installed and our config files setup, we can actually add the Tailwind CSS file to our app. To begin, create a
tailwind.css
file in the src/
folder with the following Tailwind directives:@tailwind base;
@tailwind components;
@tailwind utilities;
This will pull in and compile the styles for us automatically when we build our Tailwind output. It's also useful to note that this file is where we could add our own Tailwind custom utilities if we wanted. For more information on adding new utilities, click here.
Finally, let's import the output file (which we will create via the build scripts in the next section) to our
app.js
so it is available globally throughout our app.import './tailwind.output.css';
You may need to change the path if your
app.js
isn't directly in the root of the source folder, as that is where this will output by default.The last step to get Tailwind working is to create a couple of quick build scripts to ensure that the output file gets built. Let's add the following to our scripts section in
package.json
:"tailwind:build": "NODE_ENV=production postcss src/tailwind.css -o src/tailwind.output.css",
"tailwind:dev": "postcss src/tailwind.css -o src/tailwind.output.css",
"prestart": "npm run tailwind:dev",
"prebuild": "npm run tailwind:build",
There are two separate Tailwind scripts we created. The first
:build
one is for production builds (i.e. run the PurgeCSS we defined above), whereas the second one is a dev build which keeps the full Tailwind CSS since we want to access all the styles as we're developing.The next part is the
prestart
where we run the dev build prior to staring up the React development server. You could take it a step further and add a watch script that checks for changes to tailwind.css
, but I've found that additions / changes to this file are pretty rare, so I simply restart the dev server if this comes up.Finally, the last part is the
prebuild
script which runs the production (purged) Tailwind build which will result in a CSS file that only contains styles we've actually used in our app.That's it! You should now be up and running with Tailwind in your React app. If you're looking to take it a step further, in the next section I'll go over some design patterns and common gotchas when dealing with Tailwind in React.
Now that we have Tailwind running within our app, when we can discuss some design patterns that make it easy to work with in React components.
What Not To Do
Before jumping into some patterns, I think it's worth mentioning what not to do. How you decide to use Tailwind in your app is ultimately up to your preference, but there is one common mistake that can quickly get you into some trouble and it relates to the purging of the unused CSS as I mentioned above.
Let's look at an example to illustrate the issue. Assume we have a basic input component as follows:
<input className="border-2 border-gray-300 p-2" type="text" />
Now, let's say we want to conditionally change the border to red if there is an error somewhere. If we have an
error
state / prop variable, we may be tempted to do something like:<input
className={`border-2 border-${error ? 'red-500' : 'gray-300'} p-2`}
type="text"
/>
At first glance, this looks correct. In fact, it would render the correct classes and work just fine in development. However, once you build the production version of your app, you would notice these styles are missing, but why is that? As mentioned earlier, PurgeCSS runs against our production build to create the smallest Tailwind CSS file possible. It simply looks at the file types your provided and tries to find content matching the Regex. In this case, when it checks this component file, it won't match on
border-red-500
or border-gray-300
because they are built dynamically.So, how do we avoid this? The easiest way is to always ensure when writing Tailwind classes in your code, the full class name is there and isn't built dynamically. Therefore, using the above example, we get:
<input
className={`border-2 ${error ? 'border-red-500' : 'border-gray-300'} p-2`}
type="text"
/>
Now when PurgeCSS runs against this file, it will keep all of our added classes.
Let's take a look at some basic Tailwind patterns we can use in our app now that we know what not to do.
A pattern I've been using for a little while now is one that involves defining all of your classes above the component in an object with strings for each element.
For example, here is a basic component:
Using the above pattern, the component code would look like:
const classes = {
wrapper: 'border-2 m-16 p-16',
title: 'text-gray-800 text-xl text-center font-bold',
};
const BasicComponent = () => {
return (
<div className={classes.wrapper}>
<h1 className={classes.title}>Basic Component</h1>
</div>
);
};
This basic pattern has a couple of nice benefits:
With that said, what do we do when we get more advanced components that need to change as state or props change in our app? Let's take a look at a more advanced pattern that expands on this.
Now that we have a basic pattern involving the definition of all our classes above our component, we can take it a step further when we start to have more dynamic components.
For example, here is a more dynamic component:
In the above screenshot, both the button and the subtitle text change when the button is clicked on. Let's look at the code for this component:
const classes = {
wrapper: 'border-2 m-16 p-16 text-center',
title: 'text-gray-800 text-xl font-bold',
description: (active) =>
`my-6 ${active ? 'text-red-900 font-medium' : 'text-gray-800'}`,
button: 'py-2 px-4 bg-gray-100 border-2 focus:outline-none',
buttonActive: 'bg-gray-400',
};
const AdvancedComponent = () => {
const [active, setActive] = useState(false);
return (
<div className={classes.wrapper}>
<h1 className={classes.title}>Advanced Component</h1>
<p className={classes.description(active)}>
I change based on the button click state.
</p>
<button
className={clsx([classes.button, active && classes.buttonActive])}
onClick={() => setActive((prevState) => !prevState)}
>
Click Me
</button>
</div>
);
};
The first thing to notice is the
description
class. Unlike the others, this one is a function that takes in a parameter (or multiple) and outputs a slightly different class (text color and font weight in this case) string as a result. This allows you to maintain the benefits of the basic design pattern, but also add in a conditional aspect as well. It's also import to note, the classes are still fully defined as well so PurgeCSS will still have no issues.Another method of creating dynamic classes while maintaining the basic pattern is what was done with the
button
and buttonActive
classes. In this case, the clsx
module was used to dynamically add classes based on conditions. The classes.button
class was added without conditions (the base styles), however classes.buttonActive
is only added if active
is true. This method also works well when you have a number of conditionally added classes to an element or if your classes are just getting a little out-of-hand and you want to break them down a bit. Once again, PurgeCSS will work fine with this method as the underlying classes are fully defined.I hope this helped get you up and running with Tailwind on your React app and gives you some inspiration on how you can use it within your components in a way that makes it easier to work with.
If you have any questions, suggestions, or run into any issues, feel free to reach out on Twitter.
You can find all source code for this post in a starter template here.
Previously published at https://rfitz.io/blog/tailwind-with-react/