(Previous versions of this tutorial covered adding Tailwind to Phoenix 1.6 apps using Node and PostCSS and Phoenix 1.4 and 1.5 apps using webpack.)
Recently the Phoenix team released a Tailwind Elixir library that installs and runs a standalone Tailwind CLI for your target system. This means you can now use Tailwind CSS in Phoenix apps without a dependency on Node. 🙌
I’ve been using Tailwind CSS since the early days, and this new approach is by far the best. You get a simpler, faster, more transparent, and more predictable Tailwind integration.
However, I was initially hestitant to make the jump to the Tailwind CLI because it doesn’t include the functionality of the @tailwind/nesting
NPM package. And for educational apps, I often use a combination of CSS nesting and Tailwind’s @apply
directive to avoid using a lot of Tailwind utility classes in templates. Thankfully I discovered Michael Crumm’s excellent DartSass Elixir library and came up with a way to use it in the CSS build pipeline.
Anyway, here’s how I’m setting up my Phoenix apps with Tailwind and DartSass these days…
1. Install the Tailwind Elixir Library
First, add the tailwind
Elixir library dependency to the mix.exs
file:
defp deps do
[
...,
{:tailwind, "~> 0.1", runtime: Mix.env() == :dev}
]
end
And install it:
mix deps.get
2. Configure Tailwind
Then in the config/config.exs
file, configure the tailwind
library to use your preferred version
of Tailwind CSS with a default
execution profile like so:
config :tailwind,
version: "3.0.24",
default: [
args: ~w(
--config=tailwind.config.js
--input=css/app.css
--output=../priv/static/assets/app.css
),
cd: Path.expand("../assets", __DIR__)
]
It takes the assets/css/app.css
as the input file and outputs the generated CSS to priv/static/assets/app.css
. And it expects a tailwind.config.js
file in the assets
directory, which we’ll generate next.
3. Install Tailwind CSS
Run the following Mix task to download and install a pre-built binary of the Tailwind CLI for your target platform:
mix tailwind.install
Running this Mix task also generates a config file named tailwind.config.js
in the assets
directory. Tailwind looks for this file by default for any customizations.
The generated config file is already tailored for Phoenix apps in that the content
option includes the standard paths to the JavaScript files, view modules, and template files. Tailwind scans these files looking for any Tailwind utility classes to include in the generated CSS file:
content: [
'./js/**/*.js',
'../lib/*_web.ex',
'../lib/*_web/**/*.*ex'
],
The installer also modifies your assets/css/app.css
file to import Tailwind CSS base styles, component classes, and utility classes:
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
It also removes the following line so the default Phoenix styles aren’t used:
@import "./phoenix.css";
Finally, the installer removes the following line from the assets/js/app.js
file since the Tailwind CLI will be used as the CSS build pipeline rather than esbuild
:
import "../css/app.css"
4. Watch For Changes In Development
During development, you need to kick off the CSS build process anytime changes are made to relevant files.
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 a tailwind
watcher that invokes the install_and_run
function of the Tailwind module using the default
execution profile to bundle the application’s CSS:
config :my_app, MyAppWeb.Endpoint,
...,
watchers: [
...,
tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
]
5. Build CSS In Production
You also need to kick off the CSS build process when the application is deployed to a production environment. Basically that means running the same default
execution profile as we did in the development environment. But instead of running that command as part of a watcher, the command needs to run whenever the assets.deploy
Mix task is run.
To do that, in mix.exs
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 the CSS bundle, add a step that runs tailwind
with the default
execution profile and the minify
option to minify the generated CSS, like so:
"assets.deploy": [
"esbuild default --minify",
"tailwind default --minify",
"phx.digest"
]
6. Style Content Using Tailwind Utility Classes
Now you’re ready fire up the Phoenix server and use Tailwind utility classes to style content. For example, adding this to a view template 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-blue-500
, for example, and after saving the file your browser should automatically refresh to show a blue heading.
If you then peek at the generated CSS in priv/static/assets/app.css
, you’ll see it includes all the Tailwind base styles and component classes, but only the utility classes you’ve used: text-blue-500
, text-5xl
, font-bold
, and text-center
in this case.
Nested CSS with DartSass
Now imagine you need to reuse some styles across multiple template files. For example, let’s suppose it’s a simple content card with a title and description.
To do that, in the css/app.css
file you could define card-specific CSS rules using Tailwind’s @apply
directive, like so:
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
.card {
@apply p-4 bg-blue-200 text-blue-800 rounded-lg;
}
.card .title {
@apply text-2xl tracking-tight font-bold;
}
.card .description {
@apply pt-4 text-lg font-medium;
}
.card .description a {
@apply underline text-blue-600;
}
That’ll work, but it’s often handy to use nested rules for related styles like this. Here’s the equivalent:
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
.card {
@apply p-4 bg-blue-200 text-blue-800 rounded-lg;
& .title {
@apply text-2xl tracking-tight font-bold;
}
& .description {
@apply pt-4 text-lg font-medium;
& a {
@apply underline text-blue-600;
}
}
}
This won’t work, however, because the Tailwind CLI doesn’t include functionality for “flattening out” the nested CSS rules. (The @tailwind/nesting
NPM package does this, but it’s not part of the Tailwind CLI.)
This is where the DartSass Elixir library comes into play. It installs and runs a dart-sass
implementation for your target system. And, among other things, Sass lets you nest CSS rules.
So instead of processing the CSS file with the Tailwind CLI alone, we also need to process it with DartSass. In other words, we need a two-stage CSS build pipeline. First, we’ll have DartSass compile our CSS to an intermediate file that has all the rules flattened. And then we’ll use that file as the input to the Tailwind CLI to process the @apply
directives.
Let’s go…
1. Prepare for Sass
First, rename app.css
to app.scss
.
And in that file replace the Tailwind @import
lines with the following @tailwind
directives:
@tailwind base;
@tailwind components;
@tailwind utilities;
Then in the assets/js/app.js
file, make sure to remove (or comment) the following line so that esbuild
doesn’t try to generate CSS:
import "../css/app.css"
2. Install the DartSass Elixir Library
Then add the dart_sass
dependency to your mix.exs
file:
defp deps do
[
...,
{:dart_sass, "~> 0.5", runtime: Mix.env() == :dev}
]
end
And install it:
mix deps.get
3. Configure DartSass
Then in your config/config.exs
file, configure the dart_sass
library to use your preferred version
of dart-sass
with a default
execution profile like so:
config :dart_sass,
version: "1.49.11",
default: [
args: ~w(css/app.scss ../priv/static/assets/app.css.tailwind),
cd: Path.expand("../assets", __DIR__)
]
This uses your assets/css/app.scss
as the input file and drops the compiled CSS into the priv/static/assets/app.css.tailwind
file. That’ll be the first stage of the CSS build process.
Then change the tailwind
configuration to use the intermediate priv/static/assets/app.css.tailwind
file as its input and spit out the final CSS in /priv/static/assets/app.css
:
config :tailwind,
version: "3.0.24",
default: [
args: ~w(
--config=tailwind.config.js
--input=../priv/static/assets/app.css.tailwind
--output=../priv/static/assets/app.css
),
cd: Path.expand("../assets", __DIR__)
]
So the output of DartSass is used as the input to the Tailwind CLI. That’ll be the second stage of the CSS build process.
4. Install DartSass
Run the following Mix task to download and install a pre-built binary of the dart-sass
for your target system:
mix sass.install
5. Watch For Changes In Development
In config/dev.exs
, add a sass
watcher to the watchers
list:
config :my_app, MyAppWeb.Endpoint,
...,
watchers: [
...,
tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]},
sass: {DartSass, :install_and_run, [:default, ~w(--watch)]}
]
6. Run the Pipeline In Production
In mix.exs
, add a step to the "assets.deploy"
alias that runs sass
with the default
execution profile before the tailwind
step, like so:
"assets.deploy": [
"esbuild default --minify",
"sass default",
"tailwind default --minify",
"phx.digest"
]
7. Pre-Build Assets
Now, the first time you fire up the Phoenix server, you’ll see this:
Specified input file ../priv/static/assets/app.css.tailwind does not exist.
Compiled css/app.scss to ../priv/static/assets/app.css.tailwind.
The Tailwind CLI tried to use the intermediate app.css.tailwind
file as input, but it didn’t yet exist because DartSass ran after Tailwind. Restarting the server clears up the problem, but that’s clunky. And reordering the watchers
doesn’t fix it.
One way to get around this first-time server boot problem is to pre-build all the assets before running the server. To do that, in mix.exs
add a new "assets.build"
alias that builds the assets by running sass
before tailwind
, like so:
"assets.build": [
"esbuild default",
"sass default",
"tailwind default"
],
And then you’ll need to make sure to run mix assets.build
before firing up the server the first time.
Alternatively, since it’s common to run mix setup
on new Phoenix projects, you can add a step to the setup
alias that runs the "assets.build"
task:
setup: ["deps.get", "ecto.setup", "assets.build"],
8. Use Nested Styles
Now in your template files you should be able to use the card
, title
, and description
classes to style a content card, like so:
<div class="card">
<div class="title">
Phoenix + Tailwind
</div>
<div class="description">
A fiery wind at your back?
(<a href="#">click me</a>)
</div>
</div>
Anyway, hopefully that helps you get everything set up quickly so you can get on to making cool stuff!
🔥 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! đź‘Ť