The Pragmatic Studio

Adding Tailwind CSS to Phoenix 1.4 and 1.5

November 30, 2020

(You can find an updated tutorial for Phoenix 1.6 here.)

I’ve been using Tailwind CSS a ton recently. I wasn’t an early adopter, but after using it in earnest on a couple projects I’ve come to appreciate its simplicity. Once I internalized the nomenclature of its utility classes and embraced the constraints, my productivity soared. And designing custom web UIs became fun again. Indeed, I’m looking forward to using Tailwind on a few more projects I’ve got teed up.

This tutorial walks through how I add Tailwind CSS to my Phoenix projects. But more than that, I hope this tutorial helps you better understand how everything gets knitted together using webpack and PostCSS. The first time I tried this, I cargo-culted some configuration and cobbled it together without knowing why it worked. It’s easy to rationalize. I figured I’d need to become a webpack expert to go any deeper. Turns out you can learn a lot by just going one level below the surface.

So, before we dive into the details, let’s start by understanding the roles webpack and PostCSS play in adding Tailwind to a Phoenix app.

Webpack Meets PostCSS

Out of the box, Phoenix uses webpack to build web assets. You can think of webpack as the main pipeline for processing CSS. And by default, that pipeline is really simple as it only supports vanilla CSS. Phoenix doesn’t make any assumptions about your CSS preferences.

Tailwind CSS is a PostCSS plugin. Like webpack, PostCSS is a pipeline for processing CSS. Actually, PostCSS itself is just a tool that runs a series of PostCSS plugins which in turn transform CSS in various ways. For example, the autoprefixer plugin adds vendor prefixes such as -webkit, -moz, and -ms to CSS. And the tailwindcss plugin finds Tailwind directives in CSS and replaces them with CSS generated by Tailwind.

The key then is tying the two pipelines together—the webpack pipeline and the PostCSS pipeline—so that webpack runs the PostCSS plugins at the appropriate step in the build process. To do that we use something called a webpack “loader”. In particular, we use the postcss-loader which effectively injects the PostCSS pipeline into webpack’s pipeline.

Here’s a way to visualize the two pipelines and how they transform CSS:

PostCSS Pipeline

Anyway, keeping that in mind, we’re ready to put everything together…

1. Install Tailwind

First we need to install the tailwindcss package and its peer-dependencies using npm. Just make sure to jump into the assets directory first:

cd assets

npm install tailwindcss postcss autoprefixer postcss-loader@4.2 --save-dev

2. Add Tailwind as a PostCSS Plugin

Next we need to tell PostCSS to use the tailwindcss and autoprefixer plugins. To do that, in the assets directory create a PostCSS config file named postcss.config.js and slip this in:

module.exports = {
  plugins: {
    tailwindcss: {},
    autoprefixer: {},
  }
}

This exports an object with a plugins key whose value is an array of PostCSS plugins to use. The order is important. Remember, we’re building a CSS pipeline. Since Tailwind doesn’t include any vendor prefixes in the CSS it generates, we take care of that by adding the autoprefixer plugin after the tailwindcss plugin. The autoprefixer plugin isn’t required, but I use it in all my projects.

3. Add PostCSS to Webpack

With PostCSS ready to go, we just need to tie it into webpack using the postcss-loader. Crack open the assets/webpack.config.js file and you’ll see all the webpack configuration in its full glory. You’re looking for the CSS rule.

In Phoenix 1.4, it looks like this:

{
  test: /\.css$/,
  use: [MiniCssExtractPlugin.loader, 'css-loader']
}

In Phoenix 1.5, it’s slightly different:

{
  test: /\.[s]?css$/,
  use: [
    MiniCssExtractPlugin.loader,
    'css-loader',
    'sass-loader',
  ]
}

Regardless of the version, the CSS rule tells webpack to process CSS files using the array of loaders specified by the use property.

In Phoenix 1.4, add the postcss-loader after the css-loader like so:

use: [MiniCssExtractPlugin.loader, "css-loader", "postcss-loader"]

In Phoenix 1.5, add the postcss-loader between the css-loader and the sass-loader like so:

use: [
  MiniCssExtractPlugin.loader,
  'css-loader',
  'postcss-loader',
  'sass-loader'
]

And that effectively ties the two pipelines together.

4. Create a Tailwind Configuration File

Tailwind works great out-of-the-box and you can customize the Tailwind installation any way you like.

For Phoenix specifically, we need to customize how Tailwind purges unused styles for a production build. The first step to doing that is to create a Tailwind config file in the assets directory using the tailwindcss utility:

cd assets

npx tailwindcss init

You’ll get a default config file named tailwind.config.js that Tailwind looks for by default. And with that file in place, we can configure purging.

5. Purge Unused Styles In Production

Tailwind generates a ton of CSS utility classes by default. Every shade of color can be used for text, backgrounds, and borders. Every size on the sizing scale can be used for margin, padding, width, height, and so on. And every responsive breakpoint has it’s own copy of all these utility classes. So yeah, that adds up to a lot of CSS.

And that’s great during development because it means you have literally thousands of utility classes at your fingertips. As you’re probably already thinking though, it’s a hefty amount of CSS to put into production.

Here’s the good news: You won’t use most of those utility classes in your app and Tailwind’s purge option does a great job of removing unused CSS. In a nutshell, it analyzes your content files and CSS to determine which CSS selectors are being used, and removes unused selectors from the generated CSS. Under the hood, the purge feature uses the popular purgecss library to do all the heavy lifting.

To set up purging unused CSS in production, first add the purge option to your tailwind.config.js like so:

module.exports = {
  purge: [
    '../lib/**/*.ex',
    '../lib/**/*.leex',
    '../lib/**/*.heex',
    '../lib/**/*.eex',
    './js/**/*.js'
  ],
  theme: {},
  variants: {},
  plugins: []
};

The specified array of paths contain files that may reference any Tailwind utility class by name, which in Phoenix are all the view modules, template files, and JavaScript files.

You’ll also need to change the deploy script in package.json to set the NODE_ENV variable to production, like so:

"deploy": "NODE_ENV=production webpack --mode production"

Now when you deploy to production, Tailwind will automatically purge unused styles from your CSS.

I’ve found the out-of-the-box purging “just works” for the way I use Tailwind. If you need to customize it, you have a lot of options for controlling file size.

6. Include Tailwind In the CSS

Now we’re ready to pull all the Tailwind goodies into our application’s CSS. To do that, hop into the assets/css/app.scss file and drop in these three Tailwind directives:

@tailwind base;

@tailwind components;

@tailwind utilities;

When the webpack build process kicks in and the tailwindcss plugin gets its turn, it will replace each of these directives with CSS generated by Tailwind: it’s base styles, component classes, and utility classes.

While you’re in that file, you’ll notice it also pulls in the default Phoenix styles:

@import "./phoenix.css";

When I’m using Tailwind I generally don’t use the Phoenix styles, so I remove that line and also delete the corresponding assets/css/phoenix.css file. Totally your call.

7. Use Tailwind Utility Classes

Now fire up your Phoenix server, and you should be able to use Tailwind utility classes in any of your view templates. For example, this should give you a Phoenix-colored heading:

<h1 class="text-red-500 text-5xl font-bold text-center">Tailwind CSS</h1>

And if you peek at the generated CSS that’s been processed through PostCSS (it’s in priv/static/css/app.css) you’ll see it includes all the Tailwind base styles, component classes, and utility classes which were added by the tailwindcss plugin. As well, you’ll notice that vendor prefixes were automatically added by the autoprefixer plugin.

You’re (Probably) Done!

That’s all there is to a basic setup. And most of the time that’s all I need. But sometimes I need to go beyond the basics. In the following sections you’ll find answers to some common scenarios and use cases.

Where To Put Custom CSS

If you have an existing app that already has custom CSS, then you’ll likely want to continue using it. The easiest way to add custom CSS is to put it in a separate file and import that file at the end of the assets/css/app.css file, like so:

@tailwind base;

@tailwind components;

@tailwind utilities;

@import "./custom-styles.css";

By putting it at the end you avoid any potential specificity issues. And aren’t those fun to sort out?!

Using Custom Component Classes

It’s often handy to extract repeated combinations of Tailwind utility classes and encapsulate them in a custom component class. Buttons are a prime example because you typically use the same button style on multiple pages. That’s where Tailwind’s @apply directive comes in. For example, if your app has indigo-colored buttons you can create a custom component class named btn-indigo like so:

@tailwind base;

@tailwind components;

@tailwind utilities;

@layer components {
  .btn-indigo {
    @apply bg-indigo-700 text-white font-bold py-2 px-4 rounded;
  }
}

Wrapping your custom component classes with the @layer components { ... } directive isn’t required, but it’s recommended. Doing so moves those styles to the same place as @tailwind components which, again, avoids specificity issues.

Adding custom component classes in your top-level app.css file like this works well for most projects. You typically only need a handful of component classes. And so having everything in one file is relatively manageable.

However, on larger projects you may want to organize custom component classes in separate files. To do that, you might think (as I did) that you could just move the btn-indigo class into a ./components/buttons.css file and import it, like this:

@tailwind base;

@tailwind components;

@tailwind utilities;

@import "./components/buttons.css";

But that fails silently. It’s only when you look at the generated CSS that you notice it includes the contents of the ./components/buttons.css file verbatim. Sadly, the @apply directive in that file doesn’t get resolved as you’d expect.

Solving this involves using the postcss-import plugin, which you can install using npm:

cd assets

npm install postcss-import --save-dev

Then you need to add postcss-import as the first plugin in your postcss.config.js file, like so:

module.exports = {
  plugins: {
    "postcss-import": {},
    tailwindcss: {},
    autoprefixer: {}
  }
}

Finally, and this part is crucial, in assets/css/app.css you must import the Tailwind files using @import statements rather than @tailwind directives:

@import "tailwindcss/base";

@import "tailwindcss/components";

@import "tailwindcss/utilities";

@import "./components/buttons.css";

Why? Because postcss-import requires that all @import statements be at the top of the file. It’s for a good reason: that’s what the CSS spec dictates. So changing the @tailwind directives to @import statements keeps everything in the proper order and compliant with the spec.

I hope that helps put the wind at your back as you design custom UIs in Phoenix!

🔥 Free Phoenix LiveView Course!

Get my free Phoenix LiveView course and start building interactive, real-time features using all the facets of LiveView. This course has everything you need, assembled in the right order, and in one place! 👍

Phoenix LiveView Course