Tutorial
Send Tailwind styled Emails with Phoenix and Swoosh
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.