The Pragmatic Studio

Adding Tailwind CSS to Phoenix 1.6

September 30, 2021

Over the years, Tailwind CSS has become my go-to CSS framework. I previously wrote a tutorial on how I add Tailwind to Phoenix 1.4 and 1.5 apps which use webpack by default. Phoenix 1.6, however, uses esbuild rather than webpack by default. Although many of the steps to installing Tailwind remain the same, enough is different to warrant a new tutorial.

I’ve migrated several Phoenix apps to use esbuild and Tailwind, and couldn’t be happier with the results. With esbuild, you get a simpler, faster, more transparent, and more predictable Tailwind integration. ✨

Let’s go…

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 --save-dev

2. Add Tailwind as a PostCSS Plugin

Tailwind CSS is a PostCSS plugin, but you don’t need to know much about PostCSS to use Tailwind. You can simply think of PostCSS as a tool that runs a series of PostCSS plugins that transform CSS in various ways. For example, the autoprefixer PostCSS plugin adds vendor prefixes such as -webkit, -moz, and -ms to CSS. And the tailwindcss PostCSS plugin finds Tailwind directives in CSS and replaces them with CSS generated by Tailwind.

We just 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 here: we’re creating a CSS build pipeline. Since Tailwind doesn’t include any vendor prefixes in the CSS it generates, the autoprefixer plugin needs to run after the tailwindcss plugin.

3. Configure Tailwind for Phoenix

Next up, we need to customize the Tailwind installation for Phoenix applications. The first step to doing that is to create a Tailwind configuration file in the assets directory by 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.

Then in terms of customizations, I always want to use Tailwind’s just-in-time (JIT) compiler. Rather than generating all the Tailwind CSS utility classes during the initial build, the JIT only generates the utility classes you need based on which classes are used in your template files. The resulting CSS file is much smaller and the build time is significantly faster. For my money, using the JIT is a no-brainer.

To enable just-in-time mode, in the tailwind.config.js file set the mode option to jit and add the purge option like so:

module.exports = {
  mode: 'jit',
  purge: [
    './js/**/*.js',
    '../lib/*_web/**/*.*ex'
  ],
  theme: {},
  variants: {},
  plugins: []
};

The purge option is set to an array of file paths. The JIT scans these files looking for any Tailwind utility classes to include in the generated CSS file. In Phoenix apps, we’ll typically use Tailwind utility classes in JavaScript files, view modules, and template files. Think of purge as telling the JIT to purge all utility classes not referenced in these files from the generated CSS file. In other words, purge all the unused utility classes.

I’ve found the out-of-the-box purging strategy works great for the way I use Tailwind. If you need to customize it, you have a lot of options for controlling the size of the generated CSS. Under the hood, the purge option uses the popular purgecss library to do all the heavy lifting.

4. Include Tailwind In the CSS

Now we’re ready to pull all the Tailwind goodies into our application’s CSS.

To do that, open the existing assets/css/app.css file and you’ll notice that the first thing it does is import the default Phoenix styles for the starter application:

@import "./phoenix.css";

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

Then at the top of the file (or after the @import line if you left it in), add these three Tailwind directives:

@tailwind base;

@tailwind components;

@tailwind utilities;

When the PostCSS 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.

5. Remove CSS From the Esbuild Pipeline

This step is easy to overlook, so it deserves a section of its own!

If you look in the generated assets/js/app.js file, you’ll see that it imports the assets/css/app.css file:

import "../css/app.css"

This line causes esbuild to process the CSS file and extract it to priv/static/assets/app.css. We don’t want that to happen because it’ll cause conflicts with our PostCSS build pipeline.

So it’s important to remove that line from assets/js/app.js so that esbuild isn’t invited to the CSS build party.

6. Add a Watcher

With Tailwind ready to go, we just need to kick off the CSS build process anytime relevant changes are made during development.

To do that, crack open the config/dev.exs file and search for the list of watchers. By default, there’s an esbuild watcher that invokes the install_and_run function of the Esbuild module to bundle the application’s JavaScript:

  watchers: [
    esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]}
  ]

Add an npx watcher that runs the tailwindcss utility like so:

watchers: [
  esbuild: {Esbuild, :install_and_run, [:default, ~w(--sourcemap=inline --watch)]},
  npx: [
    "tailwindcss",
    "--input=css/app.css",
    "--output=../priv/static/assets/app.css",
    "--postcss",
    "--watch",
    cd: Path.expand("../assets", __DIR__)
  ]
]

It takes your assets/css/app.css as the input file and outputs the generated CSS to priv/static/assets/app.css, watching for changes and rebuilding as needed. Compared to using webpack, this approach is a lot more transparent.

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>

Then change text-red-500 to text-indigo-500, for example, and after saving the file your browser should automatically refresh to show an indigo-colored heading.

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

8. Building CSS in Production

We also need to kick off the CSS build process when the application is deployed to a production environment. Basically that means running the same tailwind command as we did in the development environment. But instead of running that command as part of a watcher, we want to run it whenever the mix assets.deploy task is run.

First, create a deploy script in the assets/package.json file like so:

"scripts": {
  "deploy": "NODE_ENV=production tailwindcss --postcss --minify --input=css/app.css --output=../priv/static/assets/app.css"
},

In the production environment we want to minify the generated CSS, so we’ve added the minify option.

Then we want to run the deploy script as part of the mix assets.deploy task. So open mix.exs and find the "assets.deploy" alias in the aliases function. By default, it looks like this:

"assets.deploy": ["esbuild default --minify", "phx.digest"]

It first runs esbuild with the default execution profile to bundle the js/app.js file. Then it runs the phx.digest task to digest and compress static files.

To build our CSS, add a new first step that runs the deploy script, like so:

"assets.deploy": [
  "cmd --cd assets npm run deploy",
  "esbuild default --minify",
  "phx.digest"
]

And now we’ve knitted everything together for a production build.

9. 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.

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 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 Phoenix apps!

🔥 Free Phoenix LiveView Course!

Get my free LiveView course and start building interactive, real-time features that you can drop right into your Phoenix app. Yup, all the examples were designed using Tailwind!

Phoenix LiveView Course