Components Navigation Menu

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

heex
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".

heex

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.

heex

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.

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

heex
Properties

navigation_menu/1

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

elixir
          
  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

elixir
          
  attr :class, :any, default: nil
  attr :rest, :global

  slot :inner_block, required: true
  
        

navigation_menu_footer_link/1

elixir
          
  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)