Inline CSS in Phoenix email templates

almirsarajcic

almirsarajcic

yesterday

Most email clients don’t support linked stylesheets, so you need to inline CSS for proper email rendering. Instead of manually copying styles or using external tools, you can automate this in Phoenix.

defmodule MyAppWeb.Templates.EmailTemplate do
  use MyAppWeb, :html

  def email_template(assigns) do
    ~H"""
    <!DOCTYPE html>
    <html>
      <head>
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1">
        <style>
          <%= inline_css("email.css") %>
        </style>
      </head>
      <body>
        <%= render_slot(@inner_block) %>
      </body>
    </html>
    """
  end

  defmemo inline_css(file) do
    content =
      :my_app
      |> :code.priv_dir()
      |> Path.join("static/assets/#{file}")
      |> File.read!()

    {:safe, content}
  end

  slot :inner_block, required: true
end

Now you can use it in your UserNotifier like this:

defmodule MyAppWeb.UserNotifier do
  use MyAppWeb, :html

  def deliver_confirmation_instructions(user, confirmation_url) do
    assigns = %{user: user, confirmation_url: confirmation_url}

    email_body = ~H"""
    <.email_template>
      <div class="container">
        <h1>Hi {@user.name},</h1>
        <p>Welcome to MyApp!</p>
        <p>Please confirm your email address to get started.</p>
        <a href={@confirmation_url} class="button">Verify Email</a>
      </div>
    </.email_template>
    """

    deliver(user, "Welcome to MyApp!", email_body, text_version)
  end
end

The defmemo macro comes from the https://hex.pm/packages/memoize package, which caches the CSS content in memory after the first read. Since CSS files rarely change during runtime, this prevents repeated file system access for every email sent.

If you’re unfamiliar with the defmemo macro, check out https://elixirdrops.net/d/WfCZeAod.

This keeps your email templates maintainable while ensuring consistent rendering across all email clients.