Prima.Modal (Prima v0.3.0)

View Source

A fully-managed dialog component with accessibility features and smooth transitions.

Modals can be loaded asynchronously from the server, integrated with forms, and paired with live navigation for deep linking capabilities.

Quick Start

The most basic modal requires just a trigger button and three components: .modal, .modal_overlay, and .modal_panel.

<.button phx-click={Prima.Modal.JS.open("my-modal")}>
  Open Modal
</.button>

<.modal id="my-modal">
  <.modal_overlay class="fixed inset-0 bg-gray-500/75" />
  <div class="fixed inset-0 z-10 w-screen overflow-y-auto">
    <div class="flex min-h-full items-end justify-center p-4">
      <.modal_panel id="my-modal-panel" class="relative overflow-hidden rounded-lg bg-white">
        <p>Modal content</p>
      </.modal_panel>
    </div>
  </div>
</.modal>

Focus Management

By default, the modal panel itself receives focus when opened. To focus a specific element instead, add the data-autofocus attribute:

<.modal_panel id="form-panel">
  <input type="text" data-autofocus placeholder="Name" />
</.modal_panel>

Focus is automatically restored to the triggering element when the modal closes.

Portal Behavior

By default, modals use Phoenix's portal component to teleport content to the document body, ensuring proper z-index stacking above other page content.

To render a modal inline without a portal:

<.modal id="my-modal" portal={false}>
  <.modal_overlay class="fixed inset-0 bg-gray-500/75" />
  <.modal_panel id="my-panel">
    <p>This modal renders inline</p>
  </.modal_panel>
</.modal>

Important: When portal={false}, ensure your CSS positioning allows the modal to overlay the page properly. Parent containers with overflow: hidden or position: relative may interfere with modal display. If you cannot control parent styles, use the default portal mode instead.

Modal Control Patterns

There are two main patterns for controlling modal visibility, each suited to different use cases.

Frontend Modals (Client-Side Control)

Frontend modals are always rendered in the DOM and controlled via JavaScript commands. They're ideal for simple interactions that don't require server-side state.

Opening/Closing from the Client:

<.button phx-click={Prima.Modal.JS.open("my-modal")}>Open</.button>
<.button phx-click={Prima.Modal.JS.close()}>Close</.button>

Opening/Closing from the Backend:

Use Prima.Modal.push_open/2 and Prima.Modal.push_close/2:

def handle_event("close_modal", _params, socket) do
  {:noreply, Prima.Modal.push_close(socket)}
end

To target a specific modal when multiple are present, pass the modal's id:

{:noreply, Prima.Modal.push_open(socket, "settings-modal")}

Async Modals (Server-Side Control)

Async modals are controlled by conditionally rendering the modal panel based on a LiveView assign. They're ideal when you need to load data before showing the modal.

Opening from the Client:

<.button phx-click={Prima.Modal.JS.open("async-modal") |> JS.push("load_data")}>
  Open
</.button>

def handle_event("load_data", _params, socket) do
  {:noreply, assign(socket, show_modal: true)}
end

<.modal_panel :if={@show_modal} id="async-modal-panel">
  <!-- Content -->
</.modal_panel>

Closing from the Client:

User clicks close button, which triggers backend update via on_close:

<.modal on_close={JS.push("close_modal")}>
  <!-- ... -->
</.modal>

def handle_event("close_modal", _params, socket) do
  {:noreply, assign(socket, show_modal: false)}
end

Closing from the Backend:

Simply update the assign to remove the panel:

def handle_event("submit_form", params, socket) do
  # Process form...
  {:noreply, assign(socket, show_modal: false)}
end

Summary

Functions

The main modal container component.

A loading indicator component for async modals.

A backdrop overlay for the modal.

The main content panel of the modal.

A title component for the modal.

Pushes an event to close a modal from the backend.

Pushes an event to open a modal from the backend.

Functions

modal(assigns)

The main modal container component.

This component manages the modal's visibility state and provides the foundation for other modal components. It automatically handles focus management, escape key handling, and accessibility attributes.

Attributes

  • id (required) - Unique identifier for the modal
  • class - Additional CSS classes to apply
  • on_close - JavaScript commands to execute when modal closes
  • show - Boolean indicating initial visibility state
  • portal - Boolean controlling portal usage (default: true). When true (default), modal is teleported to document body for proper z-index stacking. When false, modal renders in its natural DOM position.

Example

<.modal id="my-modal">
  <.modal_overlay />
  <.modal_panel id="my-panel">
    Modal content
  </.modal_panel>
</.modal>

Attributes

  • id (:string) (required)
  • class (:string) - Defaults to "".
  • on_close (Phoenix.LiveView.JS) - Defaults to %Phoenix.LiveView.JS{ops: []}.
  • show (:boolean) - Defaults to false.
  • portal (:boolean) - Defaults to true.

Slots

  • inner_block

push_close(socket, id \\ nil)

Pushes an event to close a modal from the backend.

Use this in LiveView event handlers to close a modal via push_event/3.

Parameters

  • socket - The LiveView socket
  • id - Optional modal ID to target a specific modal

Examples

# Close a specific modal
def handle_event("dismiss_notification", _params, socket) do
  {:noreply, Prima.Modal.push_close(socket, "notification-modal")}
end

# Close all modals (when id is nil)
def handle_event("close_modals", _params, socket) do
  {:noreply, Prima.Modal.push_close(socket)}
end

push_open(socket, id \\ nil)

Pushes an event to open a modal from the backend.

Use this in LiveView event handlers to open a modal via push_event/3.

Parameters

  • socket - The LiveView socket
  • id - Optional modal ID to target a specific modal

Examples

# Open a specific modal
def handle_event("show_settings", _params, socket) do
  {:noreply, Prima.Modal.push_open(socket, "settings-modal")}
end

# Open all modals (when id is nil)
def handle_event("show_modal", _params, socket) do
  {:noreply, Prima.Modal.push_open(socket)}
end