(Previous versions of this tutorial covered adding Tailwind CSS to Phoenix 1.6 apps.)
Phoenix 1.7 includes everything you need to use Tailwind CSS by default, with no dependency on Node. đ
Hereâs what you can do out-of-the-box, why it works, how to nest CSS with DartSass, and how to use web and locally-installed fontsâŚ
No Muss, No Fuss
When you generate a Phoenix 1.7 app, itâs pre-configured to use Tailwind by default. So you can fire up the Phoenix server and immediately start using Tailwind utility classes to style content. For example, adding this to a template gives you a Phoenix-colored heading:
<h1 class="text-red-500 text-5xl font-bold text-center">Hello, Tailwind!</h1>
Then change text-red-500
to text-blue-500
and, after saving the file, your browser will automatically refresh to show a blue heading.
It just works! đ
Why It Works
Knowing why it works is helpful when you need to go beyond the out-of-the-box experience.
Installs the Tailwind CLI
Starting in the generated mix.exs
file, the Tailwind Elixir library is listed as a dependency:
defp deps do
[
...,
{:tailwind, "~> 0.1.8", runtime: Mix.env() == :dev},
]
end
Then when you run mix setup
to install and setup the dependencies, the setup
alias defined in the aliases
function runs the tasks in the "assets.setup"
alias:
defp aliases do
[
setup: ["deps.get", "ecto.setup", "assets.setup", "assets.build"],
"assets.setup": ["tailwind.install --if-missing", ...],
...
]
end
And the tailwind.install
task downloads and installs a pre-built binary of the Tailwind CLI for your target platform.
Configures Tailwind
Tailwind expects a few things to be in place, and the generator takes care of that, too.
The generated assets/css/app.css
file imports the Tailwind CSS base styles, component classes, and utility classes:
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
And the generated assets/tailwind.config.js
file is already customized for Phoenix apps. In particular, 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"
],
Then in the config/config.exs
file, the tailwind
library is configured to use the specified version
of Tailwind CSS with a default
execution profile like so:
config :tailwind,
version: "3.2.4",
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
, using the customizations in the assets/tailwind.config.js
file. đ
Runs the CSS Build Process
During development, the CSS build process automatically kicks off anytime changes are made to relevant files. That happens by way of a watcher
thatâs configured on the applicationâs endpoint in the config/dev.exs
file:
config :my_app, MyAppWeb.Endpoint,
...,
watchers: [
...,
tailwind: {Tailwind, :install_and_run, [:default, ~w(--watch)]}
]
The tailwind
watcher invokes the install_and_run
function of the Tailwind module using the default
execution profile to bundle the applicationâs CSS.
If you 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-red-500
, text-5xl
, font-bold
, and text-center
in this case.
The CSS build process also needs to run when the application is deployed to a production environment. Basically that means running the same default
execution profile as in the development environment. But instead of doing that via a watcher, the build command runs whenever the assets.deploy
Mix task is run. That task is defined back in the aliases
function of the mix.exs
file:
"assets.deploy": [
"tailwind default --minify",
...
]
It runs tailwind
with the default
execution profile and the minify
option to minify the generated CSS for production. đď¸
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 that has a title and description:
<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>
In the css/app.css
file, you can define card-specific CSS rules using @apply
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. For comparison, hereâs the equivalent using nested rules:
@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;
}
}
}
But that wonât work 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 Michael Crummâs excellent 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.
To make this work requires a two-stage CSS build pipeline. First, DartSass needs to compile the CSS to an intermediate file that has all the nested rules flattened out. Then that file can be used as the input to the Tailwind CLI to process the @apply
directives. đŻ
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;
2. Install the DartSass Elixir Library
Then add the dart_sass
dependency to your mix.exs
file:
defp deps do
[
...,
{:dart_sass, "~> 0.5.1", 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.54.5",
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.2.4",
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 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
, change the "assets.deploy"
alias to run sass
with the default
execution profile before the tailwind
step:
"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
change the "assets.build"
alias that builds the assets so that it runs sass
before tailwind
, like so:
"assets.build": [
"esbuild default",
"sass default",
"tailwind default"
],
The setup
alias already takes care of running the "assets.build"
task, so the assets will get pre-built when mix setup
is run. Alternatively, you can run mix assets.build
before firing up the server for the first time.
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:
<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>
Using Fonts
By default, Tailwind provides three cross-browser font families. Want to spice things up with web fonts (Google Fonts) or locally-installed fonts? đśď¸
Hereâs howâŚ
Google Fonts
Suppose you want to use the popular Raleway Google Font.
-
In your
assets/css/app.css
file (orapp.scss
if youâre using Sass), import the font after the Tailwind imports:// tailwind imports @import url("https://fonts.googleapis.com/css2?family=Raleway&display=swap");
-
Then in
tailwind.config.js
, add the font to thetheme.extend.fontFamily
section, like so:theme: { extend: { fontFamily: { 'raleway': ['Raleway', 'sans-serif'] }, }, }
This extends the default Tailwind
fontFamily
configuration to include a newfont-raleway
utility class, in addition to the default Tailwind font classes. -
Now in your template files you should be able to use the
font-raleway
utility to apply the Raleway font to text:<div class="text-3xl font-raleway"> Sorry to interrupt the festivities, Dave, but I think we've got a problem. </div>
Locally-Installed Fonts
Sometimes you want to serve a font locally from your Phoenix server. For example, suppose you fancy the Space Grotesk font. đž
-
Download it and locate the
SpaceGrotesk-VariableFont_wght.ttf
file. Space Grotesk is a variable font, and all the styles are contained in this single file. -
Drop that file in the
priv/static/fonts
directory of your Phoenix application directory. -
Then in your CSS file, import the font using the
@font-face
CSS rule, like so:@font-face { font-family: "SpaceGrotesk"; src: url("/fonts/SpaceGrotesk-VariableFont_wght.ttf") format("truetype"); }
-
Finally, in
tailwind.config.js
add the font to thetheme.extend.fontFamily
section:theme: { extend: { fontFamily: { "space-grotesk": ['SpaceGrotesk', 'sans-serif'] }, }, },
-
In addition to the default Tailwind font classes, you now have a
font-space-grotesk
utility class which you can use to apply the SpaceGrotesk font to text:<div class="text-3xl font-semibold font-space-grotesk"> I'm sorry Dave, I'm afraid I can't do that. </div>
Hopefully that helps you start using Tailwind 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! đ