Tutorial

Send Tailwind styled Emails with Phoenix and Swoosh

swooshtailwindemail

This tutorial came up as a way to style an email sendout with Tailwind classes.The reason I want to do this is that styling emails is a pain and I use Tailwind every day and that makes me really productive. So, the goal is to transfer the workflow over to when styling emails.

To be fair though, I don't directly use Tailwind but use my most used utility classes and made it so that its styled inlined with the Premailex library.

Step 1 - Install Premailex library

Premailex is a library that reads the stylesheet from the html inside the <style> tags in the html and applies the styling as inline styles on the HTML tags.

To get started, add the library to the mix.exs file.

There is also another library needed for this tutorial, phoenix_swoosh, that allows you to set HTML templates as well as text bodies for you emails. Without phoenix_html, phoenix_swoosh only works with plain text templates.

def deps do
  [
    {:premailex, "~> 0.3.0"},
    {:phoenix_swoosh, "~> 1.0"},
  ]
end

and then complete the installation with mix deps.get

Step 2 - Use Premailex in the mail pipeline

Do include Premailex in the mailer pipeline, I need to define the function called premail/1 and define it like below. Note that another benefit with Premailex is that it can automatically create a text base version of the html email.

# lib/screen_builder/mailer.ex
defmodule ScreenBuilder.Mailer do
  @moduledoc false
  use Swoosh.Mailer, otp_app: :screen_builder

  import Swoosh.Email, only: [html_body: 2, text_body: 2]

  # Inline CSS so it works in all browsers
  def premail(email) do
    html = Premailex.to_inline_css(email.html_body)
    text = Premailex.to_text(email.html_body)

    email
    |> html_body(html)
    |> text_body(text)
  end
end

In the notifier, I need to do two things. First make use of Phoenix.Swoosh. That means that I need to define an Email view and a layout. Pointing the view to a module tells Phoenix.Swoosh where to look for the heex files with the email content as well as the layout used for rendering the email.

Next step is to include premail function in the mailer pipeline. It needs to be put last.

# lib/screen_builder/accounts/invitation_notifier.ex
defmodule ScreenBuilder.Accounts.InvitationNotifier do
  use Phoenix.Swoosh, view: ScreenBuilderWeb.Emails, layout: {ScreenBuilderWeb.Emails, :layout}

  import Swoosh.Email
  import ScreenBuilder.Mailer, only: [base_email: 0, premail: 1]

  @doc """
  This email is used from the Teams module to invite a new user to an account
  """
  def invite_user_email(%{email: email, url: url}) do
    new()
    |> from("from@example.com")
    |> subject("Invited to join")
    |> to(email)
    |> render_body("invite_user.html", title: "Invited to join", url: url)
    |> premail()
  end
end

Step 3 - Define the View Module

The View Module work with Phoenix 1.7 and is used to specify where the templates live.

defmodule ScreenBuilderWeb.Emails do
  use Phoenix.View,
    root: "lib/screen_builder_web",
    namespace: ScreenBuilderWeb

  use Phoenix.Component
end

This tells Phoenix.Swoosh to look for the templates in the folder lib/screen_builder_web/emails/ Note that I include Phoenix.Component so I can use custom components.

In there, I define both the layout.text.heex and layout.html.heex .

A simple HTML Email layout can look like this.

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
  <head>
    <meta name="viewport" content="width=device-width" />
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    <title><%= assigns[:title] %></title>
    <style>
    </style>
  </head>
  <body>
    <table class="body-wrap">
      <tr>
        <td></td>
        <td class="container" width="600">
          <div class="content">
            <table class="main" width="100%" cellpadding="0" cellspacing="0">
              <tr>
                <td class="content-wrap">
                  <%= @inner_content %>
                </td>
              </tr>
            </table>
            <div class="footer">
              <table width="100%">
                <tr>
                  <td class="aligncenter content-block">
                    Copyright <%= DateTime.utc_now.year %>. All rights reserved.<br>
                  </td>
                </tr>
              </table>
            </div>
          </div>
        </td>
        <td></td>
      </tr>
    </table>
  </body>
</html>

Note that for now, I have an empty <style> tag in the header.

Step 4 - Style the image with Tailwind

I generate the most used Tailwind utility classes so I can inline them in the email like this:

defmodule ScreenbuilderWeb.EmailStyles do
  def utility_classes do
    (
      build_typography() ++
      build_margin_padding_size() ++
      build_colors() ++
      build_borders()
    )
    |> Enum.concat([
      ".block {display: block;}",
      ".inline-block {display: inline-block;}",
      ".inline {display: inline;}",
      ".clear-both {clear: both;}",
    ])
    |> Enum.join()
  end

  ## Some styles omittes, see link for full file
end

You can see the entire file on the Github gist here

Then I update the layout and add the styles in the email layout header:

 <style>
    <%= Phoenix.HTML.raw ScreenbuilderWeb.EmailStyles.utility_classes() %>
 </style>

This will just output all the generated classnames in the file. With that in place, I can finally update the email layout with the Tailwind classes.

<body class="w-full h-full font-sans text-base antialiased bg-gray-100">
  <table class="w-full bg-gray-100">
    <tr>
      <td></td>
      <td class="container" width="600">
        <div class="block max-w-2xl p-6 mx-auto">
          <table class="mt-6 bg-white border-2 border-gray-200 border-solid rounded-lg" width="100%" cellpadding="0" cellspacing="0">
            <tr>
              <td class="p-6 font-sans text-base">
                <%= @inner_content %>
              </td>
            </tr>
          </table>
          <% # FOOTER %>
          <div class="w-full p-5 text-gray-400 clear-both">
            <table width="100%">
              <tr>
                <td class="pb-6 text-sm font-light text-center">
                  Copyright <%= DateTime.utc_now.year %>. All rights reserved.<br>
                </td>
              </tr>
            </table>
          </div>
        </div>
      </td>
      <td></td>
    </tr>
  </table>
</body>

To test this, I will also include this in a notifier. The email I want to use this on is an invite user to the team email. It should have a clear call to action.

<h2 class="mb-8 text-3xl text-center text-gray-600">
  You are invited to join the team
</h2>

<p class="pb-6">Please use the following link to join</p>

<p class="pb-6 text-center">
  <.link href={@url} class="inline-block pt-2 pb-2 pl-3 pr-3 text-xs font-semibold text-blue-100 no-underline uppercase bg-blue-700 border border-blue-500 rounded">
    Click to join
  </.link>
</p>

<p>You can disregard this email if you think it was sent to you by mistake.</p>

When I test this and inspect it in the development mailbox, it all looks great. At least, I can see that my styles was applied.

However, there is one more thing to check. When sending emails, the best way to use styling is to inline the styles instead of using classes that references the CSS in the style-tags. If I inspect this, I can see that the styles are indeed inlined by Premailex

Success! Next and last optional step is to write a purger that removes the styles from the header after the premailer step. But I will leave that for another day.


Related Tutorials

Published 11 Jul - 2020
Updated 22 May - 2021

Create a reusable modal with LiveView Component

To reduce duplicity and complexity in your apps, Phoenix LiveView comes with the possibility to use reusable components. Each component can have its..

Published 04 May - 2021
Updated 05 May

How to combine Phoenix LiveView with Alpine.js

No matter how great Phoenix LiveView is, there is still some use case for sprinking some JS in your app to improve UX. For example, tabs, dropdowns,..