The Pragmatic Studio

Using Tailwind CSS in Phoenix 1.7

February 14, 2023

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

  1. In your assets/css/app.css file (or app.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");
    
  2. Then in tailwind.config.js, add the font to the theme.extend.fontFamily section, like so:

    theme: {
      extend: {
        fontFamily: {
          'raleway': ['Raleway', 'sans-serif']
        },
      },
    }
    

    This extends the default Tailwind fontFamily configuration to include a new font-raleway utility class, in addition to the default Tailwind font classes.

  3. 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. 👾

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

  2. Drop that file in the priv/static/fonts directory of your Phoenix application directory.

  3. 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");
    }
    
  4. Finally, in tailwind.config.js add the font to the theme.extend.fontFamily section:

    theme: {
      extend: {
        fontFamily: {
          "space-grotesk": ['SpaceGrotesk', 'sans-serif']
        },
      },
    },
    
  5. 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! 👍

Phoenix LiveView Course