PUI.Dialog (pui v1.0.0-alpha.32)

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:

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

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

Custom 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

AttributeTypeDefaultDescription
idstringrequiredUnique identifier for the dialog
showbooleanfalseControl visibility from server
alertbooleanfalsePrevent backdrop click dismiss
sizestring"md"Max width: "sm", "md", "lg", "xl"
titlestringnilOptional built-in title for the default dialog header
show_closebooleantrueShow the built-in close button on default dialogs
on_cancelJS%JS{}JS command to run on cancel

Slots

SlotDescription
inner_blockMain dialog body content (scrolls when needed in the default layout)
footerOptional fixed footer for actions in the default layout
triggerButton/element to open dialog (receives phx-click attr)
contentOverride content container (receives attrs and hide/show)

Summary

Functions

backdrop(assigns)

Attributes

  • class (:string) - Defaults to "".
  • is_unstyled (:boolean) - Defaults to false.
  • Global attributes are accepted.

Slots

  • inner_block

content(assigns)

Attributes

  • id (:string) (required)
  • class (:string) - Defaults to "".
  • is_unstyled (:boolean) - Defaults to false.
  • title (:string) - Defaults to nil.
  • show_close (:boolean) - Defaults to true.
  • hide (Phoenix.LiveView.JS) - Defaults to %Phoenix.LiveView.JS{ops: []}.
  • Global attributes are accepted.

Slots

  • inner_block
  • footer

dialog(assigns)

Attributes

  • id (:string) (required)
  • on_cancel (Phoenix.LiveView.JS) - Defaults to %Phoenix.LiveView.JS{ops: []}.
  • alert (:boolean) - Defaults to false.
  • show (:boolean) - Control dialog visibility from server. Defaults to false.
  • size (:string) - Defaults to "md". Must be one of "sm", "md", "lg", or "xl".
  • title (:string) - Defaults to nil.
  • show_close (:boolean) - Defaults to true.
  • 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_block
  • footer - Optional fixed footer for the default dialog layout.
  • trigger
  • content - To override the content container.

hide_dialog(id)

show_dialog(id)