Components Layouts
Petal Framework

Layouts

A stylish, responsive layout system for your web app. Choose from 3 different layouts, each with a light and dark version.
Sidebar layout
Sidebar layout
Open demo
You need to be a Pro member to use this component.

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.

Sidebar layout
Light version
Sidebar layout
Dark version
View demo

A quick example:

Learn more about:

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>

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 -
:slot -
sidebar :slot -
[all additional properties] Unused.

Stacked layout

Stacked layout light
Light version
Stacked layout dark
Dark version
View demo

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.

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 -
: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:

app.html.heex
          <.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

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 the patch 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"
      }
    ]
  }
]

Nested menu items

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"
      }
    ]
  }
]

Grouped menu items

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.