Pro Layouts & Menus
Viewing latest docs.
Switch version: v3

Layouts & Menus

The layout component

Petal Pro gives the developer a <.layout> component, which takes a type attribute and renders a layout based on that type. Eg:

heex
<.layout current_page={:dashboard} current_user={@current_user} type="sidebar">
  <.container max_width="xl">
    <div>content</div>
  </.container>
</.layout>

It works by a simple case statement:

layout.ex
...
<%= case @type do %>
  <% "sidebar" -> %>
    <.sidebar_layout {assigns}>
      ...
    </.sidebar_layout>
  <% "public" -> %>
    <.public_layout {assigns}>
      ...
    </.public_layout>
<% end %>
...

You can maintain this file and add/remove layouts as you please.

How to modify an existing layout

Petal Pro provides two app layouts: "sidebar" and "public". These are quite configurable, but you may want to modify them further. To do so, edit or duplicate the layout file at:

/lib/petal_pro_web/components/pro_components

Here you can find sidebar_layout.ex and the public layout file.

How to add a new layout

  1. Copy <your_app>_web/components/pro_components/sidebar_layout.ex, eg: my_cool_layout.ex
  2. Review assigns and adjust your new layout — you can see in layout/1 (core_components.ex) how menu items are passed in as well as the user’s name and avatar
  3. When complete, import your layout into core_components.ex and modify the layout/1 function — eg:
heex
<%= case @type do %>
  <% "my-cool-layout" -> %>
    <.my_cool_layout {assigns}>
  1. Now in your template or live view file, simply update the type to your new layout:
heex
<.layout type="my-cool-layout" current_page={:dashboard} current_user={@current_user}>
  <.container>
    <div>content</div>
  </.container>
</.layout>

Petal Pro Layouts

This is the only app layout in v4 — the stacked (desktop header) layout has been removed. The sidebar supports a large number of menu items and works on all screen sizes.

heex
<.layout current_page={:dashboard} current_user={@current_user} type="sidebar">
  <.container>
    <div>content</div>
  </.container>
</.layout>

The sidebar layout features a full-height navigation panel on desktop. On mobile, the sidebar collapses behind a hamburger menu that slides in as an overlay.

Sidebar collapse

The sidebar can be collapsed to a narrow icon-only rail. The collapse button is designed as a flush edge tab with directional chevrons: it points left when the sidebar is expanded (indicating you can collapse it) and right when collapsed (indicating you can expand it).

Collapse state persists across page loads via Alpine.js $persist(), which stores the preference in localStorage. There are no flash or layout-shift issues on load because Alpine initialises the state synchronously from storage before the first paint — deferred transitions are used to prevent any animation flash.

On mobile (lg:hidden), the sidebar works differently: a sticky top bar with a hamburger button slides the sidebar in as an overlay rather than compressing the main content area.

Public layout

Includes a header and footer — for use in public-facing, marketing pages. The public layout is available in router.ex via the :public_layout pipeline:

elixir
# Public routes
scope "/", PetalProWeb do
  pipe_through [:browser, :public_layout]

  get "/", PageController, :landing_page
  get "/privacy", PageController, :privacy
  get "/license", PageController, :license

  live_session :public, layout: {PetalProWeb.Layouts, :public} do
    # Add public live routes here
  end
end

The public layout supports both light and dark mode out of the box.

In menus.ex there is a list of all the menu items. This can be useful when working with navbars and layouts, where you sometimes need to loop over menu items multiple times.

For all menu items, you must define a new get_link/2 function, which takes a name and current_user as the parameters. The name parameter is pattern matched and thus you statically include it in the function definition. A menu item has a name, label, path, and icon. Here’s an example:

elixir
def get_link(:register, _current_user) do
  %{
    name: :register,
    label: "Register",
    path: Routes.user_registration_path(Endpoint, :new),
    icon: "hero-clipboard-list"
  }
end

You can use the current_user param to conditionally display a menu item. For example, here we only show the item to admins:

elixir
def get_link(:admin_users = name, current_user) do
  if Helpers.is_admin?(current_user) do
    %{
      name: name,
      label: "Users",
      path: Routes.admin_users_path(Endpoint, :index),
      icon: "hero-users"
    }
  else
    nil
  end
end

You can build menu item lists to give to layouts. The two core menu lists — one for the sidebar and one for the public header — are defined in menus.ex and passed to the layout component as assigns.

Never hardcode menu items inline in templates — always define them in PetalProWeb.Menus.