PUI.Dialog
(pui v1.0.0-alpha.29)
Copy Markdown
A modal dialog component for LiveView applications.
Basic Usage
The simplest way to use a dialog is with a trigger button:
<.dialog id="my-dialog" title="Dialog title">
<:trigger :let={attr}>
<.button {attr}>Open Dialog</.button>
</:trigger>
<p>Dialog content goes here.</p>
<:footer :let={%{hide: hide}}>
<div class="flex justify-end gap-2">
<.button variant="outline" phx-click={hide}>Cancel</.button>
<.button>Confirm</.button>
</div>
</:footer>
</.dialog>Accessing Hide/Show Actions
Use :let to access the hide and show JS commands:
<.dialog :let={%{hide: hide, show: show}} id="my-dialog" title="Project details">
<:trigger :let={attr}>
<.button {attr}>Open</.button>
</:trigger>
<p>Content</p>
<:footer>
<div class="flex justify-end gap-2">
<.button variant="outline" phx-click={hide}>Close</.button>
<.button phx-click={show}>Refresh focus</.button>
</div>
</:footer>
</.dialog>Server-Controlled Dialog
Control dialog visibility from your LiveView using the show attribute:
# In your LiveView
def mount(_params, _session, socket) do
{:ok, assign(socket, show_dialog: false)}
end
def handle_event("open", _, socket), do: {:noreply, assign(socket, show_dialog: true)}
def handle_event("close", _, socket), do: {:noreply, assign(socket, show_dialog: false)}
# In template - use on_cancel to sync state when dismissed via backdrop/escape
<.button phx-click="open">Open</.button>
<.dialog id="my-dialog" show={@show_dialog} on_cancel={JS.push("close")}>
<p>Server-controlled content</p>
<.button phx-click="close">Close</.button>
</.dialog>show={} vs :if={}
Two approaches for server-controlled dialogs:
| Approach | Behavior |
|---|---|
show={@visible} | Dialog stays in DOM, visibility toggled. Form state preserved, animations work. |
:if={@visible} | Dialog mounted/unmounted. Form state resets, no exit animations. |
Using show={} (recommended for most cases):
<.dialog id="dialog" show={@show_dialog} on_cancel={JS.push("close")}>
...
</.dialog>Using :if={} (when you want fresh state each time):
<.dialog :if={@show_dialog} id="dialog" show={true} on_cancel={JS.push("close")}>
...
</.dialog>Alert Dialog
Use alert={true} to prevent closing via backdrop click (escape still works):
<.dialog id="confirm-delete" title="Delete item" alert={true}>
<:trigger :let={attr}>
<.button variant="destructive" {attr}>Delete</.button>
</:trigger>
<p>Are you sure? This cannot be undone.</p>
</.dialog>Dialog Title
Use the title attribute to render a built-in dialog heading:
<.dialog id="profile-dialog" title="Edit profile">
<:trigger :let={attr}>
<.button {attr}>Edit profile</.button>
</:trigger>
<p>Update your profile details.</p>
</.dialog>The close button is shown by default. Disable it with show_close={false}:
<.dialog id="checkout-dialog" title="Checkout" show_close={false}>
<p>Review your order before continuing.</p>
</.dialog>Scrollable Body with Footer
The default dialog keeps the title and footer fixed while the body scrolls automatically:
<.dialog id="activity-dialog" title="Recent activity" size="lg">
<div class="space-y-4">
<p :for={_ <- 1..12}>Scrollable content</p>
</div>
<:footer :let={%{hide: hide}}>
<div class="flex justify-end gap-2">
<.button variant="outline" phx-click={hide}>Close</.button>
<.button>Save changes</.button>
</div>
</:footer>
</.dialog>Dialog Sizes
Control max-width with the size attribute:
<.dialog id="small" size="sm">...</.dialog> # sm:max-w-sm
<.dialog id="medium" size="md">...</.dialog> # md:max-w-md (default)
<.dialog id="large" size="lg">...</.dialog> # lg:max-w-lg
<.dialog id="xlarge" size="xl">...</.dialog> # xl:max-w-xlCustom Content Slot
Override the default content container for full customization. This bypasses the built-in title, scrollable body, and footer layout:
<.dialog id="custom">
<:trigger :let={attr}>
<.button {attr}>Open</.button>
</:trigger>
<:content :let={{attrs, %{hide: hide}}}>
<div class="my-custom-dialog-class" {attrs}>
<p>Fully customized container</p>
<.button phx-click={hide}>Close</.button>
</div>
</:content>
</.dialog>Programmatic Show/Hide
Use show_dialog/1 and hide_dialog/1 functions directly:
<.button phx-click={PUI.Dialog.show_dialog("my-dialog")}>Open</.button>
<.button phx-click={PUI.Dialog.hide_dialog("my-dialog")}>Close</.button>Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
id | string | required | Unique identifier for the dialog |
show | boolean | false | Control visibility from server |
alert | boolean | false | Prevent backdrop click dismiss |
size | string | "md" | Max width: "sm", "md", "lg", "xl" |
title | string | nil | Optional built-in title for the default dialog header |
show_close | boolean | true | Show the built-in close button on default dialogs |
on_cancel | JS | %JS{} | JS command to run on cancel |
Slots
| Slot | Description |
|---|---|
inner_block | Main dialog body content (scrolls when needed in the default layout) |
footer | Optional fixed footer for actions in the default layout |
trigger | Button/element to open dialog (receives phx-click attr) |
content | Override content container (receives attrs and hide/show) |
Summary
Functions
Attributes
class(:string) - Defaults to"".is_unstyled(:boolean) - Defaults tofalse.- Global attributes are accepted.
Slots
inner_block
Attributes
id(:string) (required)class(:string) - Defaults to"".is_unstyled(:boolean) - Defaults tofalse.title(:string) - Defaults tonil.show_close(:boolean) - Defaults totrue.hide(Phoenix.LiveView.JS) - Defaults to%Phoenix.LiveView.JS{ops: []}.- Global attributes are accepted.
Slots
inner_blockfooter
Attributes
id(:string) (required)on_cancel(Phoenix.LiveView.JS) - Defaults to%Phoenix.LiveView.JS{ops: []}.alert(:boolean) - Defaults tofalse.show(:boolean) - Control dialog visibility from server. Defaults tofalse.size(:string) - Defaults to"md". Must be one of"sm","md","lg", or"xl".title(:string) - Defaults tonil.show_close(:boolean) - Defaults totrue.variant(:string) - Defaults to"default". Must be one of"default", or"unstyled".class(:string) - Defaults to"".- Global attributes are accepted. Supports all globals plus:
["aria-label", "aria-labelledby", "aria-describedby"].
Slots
inner_blockfooter- Optional fixed footer for the default dialog layout.triggercontent- To override the content container.