Layouts
A stylish, responsive layout system for your web app. Choose from 3 different layouts, each with a light and dark version.

Sidebar layout
Our sidebar layout should suit most of your needs to get up and running quickly. We’ve tried hard to make it work out of the box, but also be customizable.


A quick example:
Learn more about:
- Customizing the header or sidebar
- Sidebar layout properties
- Installing a layout (common to all layouts)
- Menu item structure (common to all layouts)
- Handling signed in users (common to all layouts)
Customizing the header or sidebar
You can add things next to the user avatar in the header with a slot. Here’s an example:
<.sidebar_layout ...>
<:top_left>
This goes on the left side of the header
</:top_left>
<:top_right>
This goes on the right side of the header, to the left of the user dropdown.
</:top_right>
<:sidebar>
This goes in the sidebar (under the menu items).
You omit `main_menu_items` if you like and just use this.
</:sidebar>
</.sidebar_layout>
Sidebar layout properties
Defaults are indicated in bold. Required fields have * before the label. Hover over the name for docs.
Name | Type | Options/Default |
---|---|---|
current_page* | :atom | - |
main_menu_items | :list | |
user_menu_items | :list | |
avatar_src | :string | - |
current_user_name | :string | - |
sidebar_title | :string | - |
home_path | :string | / |
sidebar_width_class | :string | lg:w-64 |
sidebar_bg_class | :string | bg-white dark:bg-gray-900 |
sidebar_border_class | :string | border-gray-200 dark:border-gray-700 |
header_bg_class | :string | bg-white dark:bg-gray-900 |
header_border_class | :string | border-gray-200 dark:border-gray-700 |
inner_block* | :slot | - |
top_left | :slot | - |
top_right | :slot | - |
logo | :slot | - |
sidebar | :slot | - |
[all additional properties] | Unused. |
Stacked layout


Get up and running
You’ll notice this is called with the same arguments as the sidebar component example. That’s because the same data structure for menu items is used. Our goal is to make it easy to switch between layouts. Your job is to maintain a list of menu items.
- Customizing the header or sidebar
- Stacked layout properties
- Installing a layout (common to all layouts)
- Menu item structure (common to all layouts)
- Handling signed in users (common to all layouts)
To get it working in your application, try putting this in your layout file. You can put it in app.html.heex
or create a new one called sidebar.html.heex
. If you create a new one, then make sure to set it up properly.
Stacked layout properties
Defaults are indicated in bold. Required fields have * before the label. Hover over the name for docs.
Name | Type | Options/Default |
---|---|---|
current_page* | :atom | - |
main_menu_items | :list | |
user_menu_items | :list | |
avatar_src | :string | - |
current_user_name | :string | - |
home_path | :string | / |
container_max_width | :string | lg |
hide_active_menu_item_border | :boolean | - |
main_bg_class | :string | bg-white dark:bg-gray-900 |
header_bg_class | :string | bg-white dark:bg-gray-900 |
header_border_class | :string | border-gray-200 dark:border-gray-700 |
sticky | :boolean | - |
inner_block | :slot | - |
top_right | :slot | - |
logo* | :slot | - |
[all additional properties] | Unused. |
Installation
A layout should really be defined in one spot. Phoenix has already thought of this and gives you a “root” layout and an “app” layout. The “root” layout is rendered only on the initial request and therefore it only has access to the @conn
assign. The “app.html.heex” layout is rendered with either @conn or @socket.
Since you likely want access to the @socket
it’s better to define your layout in the “app” layout. This is called app.html.heex
or you could create a new one called sidebar.html.heex
.
If you create a new one, then make sure to let your live views know how to use it..
Here is an example of an app layout that uses the sidebar layout:
<.flash_group flash={@flash} />
<.sidebar_layout
main_menu_items={[
%{
name: :dashboard,
label: "Dashboard",
icon: :home,
path: ~p"/"
},
]}
user_menu_items={[]}
current_page={@current_page}
>
<:logo>
LOGO GOES HERE
</:logo>
<%= @inner_content %>
</.sidebar_layout>
Note that you will need @current_page
defined in every view.
For live views you can do this in your mount function. It’s good to also define a page_title
, which will update the <title>
meta tag:
def mount(_params, _session, socket) do
socket = assign(socket,
current_page: :dashboard,
page_title: "Dashboard"
)
{:ok, socket}
end
For controllers you can do this in your action function:
def index(conn, _params) do
conn
|> assign(:current_page, :dashboard)
|> assign(:page_title, "Dashboard")
|> render("index.html")
end
Menu items
main_menu_items
is a list of items that will be displayed in the main section of a layout. For the sidebar layout it will be in the sidebar. For the stacked layout, it will be in the header up top.
Each item in the list is an Elixir map with the following keys:
-
name
- The name of the menu item. This will be used to highlight the current page in the menu. -
label
- The text that will be displayed in the menu. -
icon
- The icon that will be displayed next to the label. This should be a valid Heroicon name. You can also use a raw HTML string here or pass a function that renders a component (eg.&my_cool_icon/1
). -
path
- The path that the user will be taken to when they click the menu item. -
patch_group
- An optional key to match menu items that link to one live view and thus can use thepatch
functionality
Here’s an example:
[
%{
name: :dashboard, # If this matches the `current_page` prop, it will be highlighted
label: "Dashboard",
icon: "home",
path: ~p"/"
},
%{
name: :settings,
label: "Settings",
icon: "cog",
path: ~p"/settings"
}
]
Nested menu items
You can have nested menu items that are exposed via a dropdown. The dropdown works via Alpine JS, so you’ll need to have that installed. Here’s an example:
[
%{
name: :company,
label: "Company",
icon: :building_office,
# These menu items are nested under the dropdown:
menu_items: [
%{
name: :company_reports,
label: "Reports",
icon: :chart_pie,
path: ~p"/company/reports"
},
%{
name: :company_settings,
label: "Settings",
icon: :cog_6_tooth,
path: ~p"/company/settings"
}
]
}
]
Grouped menu items
You can have multiple groups of menu items. This is useful if you want to separate your menu items into different sections. Here’s an example:
[
%{
title: "Group 1",
menu_items: [
%{
name: :company_reports,
label: "Reports",
icon: :chart_pie,
path: ~p"/company/reports"
},
]
},
%{
title: "Group 2",
menu_items: [
%{
name: :company_settings,
label: "Settings",
icon: :cog_6_tooth,
path: ~p"/company/settings"
}
]
}
]
User menu items
Note that the same menu item format applies for the user_menu_items
. These are displayed in the dropdown for the user profile in the top right of the header. You can’t do nesting or grouping there though.
Patching between menu items
If two or more menu items link to one live view, then when you travel between them a patch can be performed instead of live navigation or a full page reload. Patching can mean a faster transition between pages.
Let’s say you have a live view called YourAppWeb.SettingsLive
that handles user setting pages. You can set the patch_group
key to YourAppWeb.SettingsLive
for each menu item that links to that live view. Then when you travel between those pages, a patch will be performed. For example:
[
%{
name: :settings,
label: "Settings",
icon: "cog",
path: ~p"/settings",
patch_group: YourAppWeb.SettingsLive
},
%{
name: :settings_profile,
label: "Profile",
icon: "user",
path: ~p"/settings/profile",
patch_group: YourAppWeb.SettingsLive
},
%{
name: :settings_billing,
label: "Billing",
icon: "credit-card",
path: ~p"/settings/billing",
patch_group: YourAppWeb.SettingsLive
}
]
Note that you can use any unique atom you want.
Handling signed in users
If a user is signed in, we want to display their profile picture and name in the header. To do this, we need to pass in a few more properties to the layout.
<.sidebar_layout
avatar_src={@current_user.avatar}
current_user_name={@current_user.name}
...
/>
This assumes your user schema has an avatar
or equivalent field. If you don’t have this, you can just pass in nil
and the user’s initials will be displayed instead.