Tutorial

Nested Templates and Layouts in Phoenix Framework

This post was updated 27 Mar

templates layouts phoenix

Eventually when your site reaches a certain maturity, you look into ways to refactor the code. One is to add parts of the templates in reusable layout templates. The reason is that HTML templates can be pretty verbose and you want certain elements to look the same across the site.

This approach is an alternative to using partials and even though partials are fine, it might be a little cumbersome if you want to pass in a lot of content. Like a form or a large block of HTML.

In modern Phoenix (1.7+), the recommended way to handle this is with function components. Instead of separate view modules and template files, we define components as functions that take assigns and render HEEx markup.

STEP 1 - Create a shared components module

All the shared components will live in a module. In Phoenix 1.7+, the convention is to place reusable components in your core components module or a dedicated shared components module.

# lib/my_app_web/components/shared_components.ex
defmodule MyAppWeb.SharedComponents do
use Phoenix.Component
attr :class, :string, default: nil
slot :inner_block, required: true
def card(assigns) do
~H"""
<div class="max-w-sm rounded overflow-hidden shadow-lg">
<div class="px-6 py-4">
{render_slot(@inner_block)}
</div>
</div>
"""
end
attr :class, :string, default: nil
slot :inner_block, required: true
def card_header(assigns) do
~H"""
<div class={["bg-gray-50 py-4 px-6 border-b border-gray-200 rounded-t-lg", @class]}>
{render_slot(@inner_block)}
</div>
"""
end
attr :class, :string, default: nil
slot :inner_block, required: true
def card_body(assigns) do
~H"""
<div class={["px-8 py-8", @class]}>
{render_slot(@inner_block)}
</div>
"""
end
end

To make these components available in your templates, import them in your my_app_web.ex file:

# lib/my_app_web.ex
# Inside the `html_helpers` function, add:
import MyAppWeb.SharedComponents

STEP 2 - Start using the card component

With the components set up, I should now be able to start using the cards in different templates. I can use only the outer card like this:

<.card>
<div class="font-bold text-xl mb-2">The Coldest Sunset</div>
<p class="text-gray-700 text-base">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus quia, nulla! Maiores et perferendis eaque, exercitationem praesentium nihil.
</p>
</.card>

Or I can nest the components with card_header and card_body inside the card:

<.card>
<.card_header>
The Coldest Sunset
</.card_header>
<.card_body>
<p class="text-gray-700 text-base">
Lorem ipsum dolor sit amet, consectetur adipisicing elit. Voluptatibus quia,
nulla! Maiores et perferendis eaque, exercitationem praesentium nihil.
</p>
</.card_body>
</.card>

This approach using function components is much cleaner than the old Phoenix.View and template-based approach. You get compile-time checks, documented attributes with attr, and named slots with slot – all built into Phoenix.

Related Tutorials

NEW
Published 11 Apr

Activity Tracking in Phoenix LiveView

Most SaaS apps need some form of activity tracking — who did what and when. In this tutorial, we'll build an automatic activity tracking system that..

NEW
Published 11 Apr

Rendering an Activity Feed in Phoenix LiveView

In the previous tutorial, we built an automatic activity tracking system that records events whenever a tracked schema is inserted, updated, or dele..