Components Modals

Petal Pro is the full SaaS app this is built for

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

Modals

Modals sit on top of an applications main window and require a live view to handle their on/off state. We recommend using a new route to map to the on/off state.
Sizes
elixir
          scope "/", PetalProWeb do
  pipe_through [:browser]

  live "/", ExampleLive, :index
  live "/modal", ExampleLive, :modal
end

        
elixir
          defmodule ExampleLive do
  use PetalProWeb, :live_view

  # ...

  @impl true
  def handle_params(params, _uri, socket) do
    {:noreply, socket}
  end

  # The modal component emits this event when `PetalComponents.Modal.hide_modal()` is called.
  # This happens when the user clicks the dark background or the 'X'.
  @impl true
  def handle_event("close_modal", _, socket) do

    # Go back to the :index live action
    {:noreply, push_patch(socket, to: "/")}
  end

  # ...

end

        
heex
          <.a type="live_patch" to="/modal" label="Opens the modal" />

<%= if @live_action == :modal do %>
  <.modal max_width="sm|md|lg|xl|2xl|full" title="Modal">
    <.p>Content</.p>

    <div class="flex justify-end">
      <.button label="close" phx-click={PetalComponents.Modal.hide_modal()} />
    </div>
  </.modal>
<% end %>

        

If you’re nesting a modal inside a live component, the close event will go to the parent live view, instead of the component.

To fix this, you can change the target to be the live component:

<.modal title="Modal" close_modal_target={@myself}>
  <.p>Content</.p>
</.modal>

Underneath the hood, this gets forwarded to the JS.push function. Here’s what it looks like:

# in petal_components/modal.ex
JS.push(js, "close_modal", target: close_modal_target)
Properties
elixir
          attr :id, :string, default: "modal", doc: "modal id"
attr :hide, :boolean, default: false, doc: "modal is hidden"
attr :title, :string, default: nil, doc: "modal title"

attr :close_modal_target, :string,
  default: nil,
  doc:
    "close_modal_target allows you to target a specific live component for the close event to go to. eg: close_modal_target={@myself}"

attr :close_on_click_away, :boolean,
  default: true,
  doc: "whether the modal should close when a user clicks away"

attr :close_on_escape, :boolean,
  default: true,
  doc: "whether the modal should close when a user hits escape"

attr :hide_close_button, :boolean,
  default: false,
  doc: "whether or not the modal should have a close button in the header"

attr :on_cancel, JS,
  default: JS.exec("data-cancel-default"),
  doc:
    "a JS function to execute when the modal is closed. Defaults to pushing close_modal event"

attr :max_width, :string,
  default: "md",
  values: ["sm", "md", "lg", "xl", "2xl", "full"],
  doc: "modal max width"

attr :rest, :global
slot :inner_block, required: false