Petal Pro is the full SaaS app this is built for

Auth, billing, admin, and Claude Code integration included. One purchase, unlimited projects.

A horizontal navigation menu with flyout panels, the pattern marketing sites use for Products / Solutions / Resources menus that hold more than a simple dropdown can. It follows the W3C disclosure navigation pattern: each trigger is a button with aria-expanded and aria-controls, panels contain plain links, and Escape or clicking away closes any open panel. Interaction is click-to-toggle (touch friendly) and 100% LiveView.JS. No Alpine, no hooks.
A navigation menu is a desktop pattern. On a phone, pair it with a collapsible "hamburger" menu rather than the full horizontal nav, the way most sites do. Panels are width-clamped to the viewport, but a flyout anchored to a trigger near a screen edge can still extend past it on a small screen, so the mobile answer is a collapsed menu, not this layout shrunk down.
Basic Navigation Menu

Give the nav a unique id and one :item slot per top-level entry. An item with inner content renders a flyout trigger; compose its panel from navigation_menu_link rows, each with an icon, a title and a one-line description. An item with a to attribute renders a plain link instead. Click Products below to open the panel.

<.navigation_menu id="nav-basic">
<:item label="Products" width="md">
<.navigation_menu_link
to="#"
icon="hero-chart-bar"
title="Analytics"
description="Understand where your traffic comes from"
/>
<.navigation_menu_link
to="#"
icon="hero-cursor-arrow-rays"
title="Engagement"
description="Speak directly to your customers"
/>
<.navigation_menu_link
to="#"
icon="hero-shield-check"
title="Security"
description="Keep your customer data safe"
/>
</:item>
<:item label="Pricing" to="#" />
</.navigation_menu>
Multi-Column Panels

Panels are free-form markup, so a multi-column layout is just a grid. Set width="xl" on the item to make room, then wrap the links in grid grid-cols-2 gap-1. Width options are "sm", "md" (the default), "lg" and "xl".

<.navigation_menu id="nav-columns">
<:item label="Solutions" width="xl">
<div class="grid grid-cols-2 gap-1">
<.navigation_menu_link
to="#"
icon="hero-chart-bar"
title="Analytics"
description="Traffic and conversion reports"
/>
<.navigation_menu_link
to="#"
icon="hero-cursor-arrow-rays"
title="Engagement"
description="Email and in-app campaigns"
/>
<.navigation_menu_link
to="#"
icon="hero-arrow-path"
title="Automations"
description="Put repetitive work on autopilot"
/>
<.navigation_menu_link
to="#"
icon="hero-shield-check"
title="Security"
description="SSO, audit logs and compliance"
/>
</div>
</:item>
</.navigation_menu>
Staying on Screen

Panels anchor to the trigger's start edge and open rightward, so a left-most trigger never opens back under a sidebar. For a trigger near the right edge, set align="end" so its panel opens leftward instead and stays on screen. Panel widths clamp to the viewport so a wide panel shrinks to fit on a phone instead of overflowing, and a panel taller than the screen scrolls rather than being cut off.

<.navigation_menu id="nav-align">
<:item label="Resources" width="md" align="end">
<.navigation_menu_link to="#" icon="hero-book-open" title="Docs" description="Guides and API reference" />
<.navigation_menu_link to="#" icon="hero-academic-cap" title="Tutorials" description="Learn by building" />
</:item>
</.navigation_menu>

navigation_menu_footer renders a full-bleed strip along the bottom of a panel, made for one to three navigation_menu_footer_link entries. The usual suspects: watch a demo, contact sales.

<.navigation_menu id="nav-footer">
<:item label="Resources" width="md">
<.navigation_menu_link
to="#"
icon="hero-sparkles"
title="What's new"
description="Product updates, shipped weekly"
/>
<.navigation_menu_link
to="#"
icon="hero-chart-bar"
title="Case studies"
description="How teams use the platform"
/>
<.navigation_menu_footer>
<.navigation_menu_footer_link to="#" icon="hero-play-circle" label="Watch demo" />
<.navigation_menu_footer_link to="#" icon="hero-phone" label="Contact sales" />
</.navigation_menu_footer>
</:item>
</.navigation_menu>

An item with a to attribute skips the flyout and renders a plain link. Add current to the active item and the link gets aria-current="page", which the component CSS uses to highlight it. Plain links default to a regular anchor; set link_type to "live_patch" or "live_redirect" for LiveView navigation.

<.navigation_menu id="nav-links">
<:item label="Home" to="#" />
<:item label="Pricing" to="#" />
<:item label="Docs" to="#" current />
</.navigation_menu>
Full-Width Mega Menu

Set full_width on an item and its panel spans the nearest positioned ancestor instead of anchoring to the trigger. Give your site header class="relative" and the panel becomes a full-width mega menu beneath it. In this demo the positioned ancestor is the bordered container, so the panel stretches across it edge to edge.

<div class="relative">
<.navigation_menu id="nav-mega">
<:item label="Platform" full_width>
<div class="grid gap-1 sm:grid-cols-2 lg:grid-cols-3">
<.navigation_menu_link
to="#"
icon="hero-chart-bar"
title="Analytics"
description="Traffic, funnels and retention"
/>
<.navigation_menu_link
to="#"
icon="hero-cursor-arrow-rays"
title="Engagement"
description="Campaigns across email and in-app"
/>
<.navigation_menu_link
to="#"
icon="hero-shield-check"
title="Security"
description="SSO, roles and audit logs"
/>
<.navigation_menu_link
to="#"
icon="hero-arrow-path"
title="Automations"
description="Workflows that run themselves"
/>
<.navigation_menu_link
to="#"
icon="hero-sparkles"
title="What's new"
description="The latest releases and fixes"
/>
<.navigation_menu_link
to="#"
icon="hero-play-circle"
title="Demos"
description="See it working before you commit"
/>
</div>
</:item>
<:item label="Pricing" to="#" />
</.navigation_menu>
</div>
Properties

navigation_menu/1

attr :id, :string, required: true
attr :class, :any, default: nil, doc: "extra classes for the nav element"
attr :rest, :global
slot :item, required: true do
attr :label, :string, required: true
attr :to, :string, doc: "render a plain link instead of a flyout trigger"
attr :link_type, :string, doc: ~s(for plain links: "a" (default), "live_patch" or "live_redirect")
attr :width, :string, doc: ~s(flyout panel width: "sm", "md" (default), "lg" or "xl")
attr :align, :string, doc: ~s(which trigger edge the panel anchors to: "start" (default) or "end")
attr :full_width, :boolean, doc: "span the panel across the nearest positioned ancestor (mega menu)"
attr :current, :boolean, doc: "marks the active item with aria-current"
end

navigation_menu_link/1

attr :to, :string, required: true, doc: "link destination"
attr :title, :string, required: true
attr :description, :string, default: nil
attr :icon, :string, default: nil, doc: "heroicon name, e.g. hero-chart-bar"
attr :link_type, :string, default: "a", values: ["a", "live_patch", "live_redirect"]
attr :class, :any, default: nil
attr :rest, :global, include: ~w(target rel method download)

navigation_menu_footer/1

attr :class, :any, default: nil
attr :rest, :global
slot :inner_block, required: true

navigation_menu_footer_link/1

attr :to, :string, required: true, doc: "link destination"
attr :label, :string, required: true
attr :icon, :string, default: nil, doc: "heroicon name, e.g. hero-play-circle"
attr :link_type, :string, default: "a", values: ["a", "live_patch", "live_redirect"]
attr :rest, :global, include: ~w(target rel method download)