Prima.Modal (Prima v0.3.0)
View SourceA 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)}
endTo 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)}
endClosing 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
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 modalclass- Additional CSS classes to applyon_close- JavaScript commands to execute when modal closesshow- Boolean indicating initial visibility stateportal- 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 tofalse.portal(:boolean) - Defaults totrue.
Slots
inner_block
A loading indicator component for async modals.
This component displays while the modal is in a loading state, typically used for async modals where content is fetched from the server before displaying the main modal panel.
Example
<.modal_loader>
<div class="spinner">Loading...</div>
</.modal_loader>Slots
inner_block
A backdrop overlay for the modal.
This component provides the semi-transparent overlay that appears behind the modal content. It supports customizable enter/leave transitions.
Attributes
class- CSS classes for styling the overlaytransition_enter- Transition configuration for showing the overlaytransition_leave- Transition configuration for hiding the overlay
Example
<.modal_overlay
class="fixed inset-0 bg-gray-500/75"
transition_enter={{"ease-out duration-300", "opacity-0", "opacity-100"}}
transition_leave={{"ease-in duration-200", "opacity-100", "opacity-0"}}
/>Attributes
class(:string) - Defaults to"".transition_enter(:any) - Defaults tonil.transition_leave(:any) - Defaults tonil.
The main content panel of the modal.
This component contains the actual modal content and handles focus management, keyboard navigation (escape key), and click-away behavior. It automatically wraps content in a focus trap for accessibility.
Focus Management
When a modal opens, focus is managed as follows:
- If an element with
data-autofocusattribute exists within the modal, that element will receive focus - Otherwise, the first focusable element within the modal panel will receive focus automatically
When the modal closes, focus is automatically restored to the element that triggered the modal.
Attributes
id(required) - Unique identifier for the panelclass- CSS classes for styling the paneltransition_enter- Transition configuration for showing the paneltransition_leave- Transition configuration for hiding the panel
Examples
# Basic modal - first focusable element receives focus by default
<.modal_panel
id="my-panel"
class="relative overflow-hidden rounded-lg bg-white"
transition_enter={{"ease-out duration-300", "opacity-0 scale-95", "opacity-100 scale-100"}}
>
<h2>Modal Title</h2>
<p>Modal content goes here</p>
<button phx-click={Modal.close()}>Close</button>
</.modal_panel>
# Modal with autofocus on a specific element
<.modal_panel id="form-panel" class="...">
<h2>Edit Profile</h2>
<input type="text" name="name" data-autofocus placeholder="Enter name" />
<button type="submit">Save</button>
</.modal_panel>Attributes
id(:string) (required)class(:string) - Defaults to"".transition_enter(:any) - Defaults tonil.transition_leave(:any) - Defaults tonil.
Slots
inner_block
A title component for the modal.
This component provides the accessible title for the modal dialog. It automatically generates an ID that will be referenced by the modal container's aria-labelledby attribute.
Attributes
class- CSS classes for styling the titleas- Custom function component to render instead of default h3 tag
Example
<.modal_title class="text-lg font-semibold">
Confirm Action
</.modal_title>
# With custom component
<.modal_title as={&my_custom_heading/1}>
Custom Title
</.modal_title>Attributes
class(:string) - Defaults tonil.as(:any) - Defaults to"h3".
Slots
inner_block(required)
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 socketid- 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
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 socketid- 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