The Dialog component provides accessible modal dialogs built on the native <dialog> element. It supports built-in titles, an optional close button, fixed footers, server-controlled visibility, multiple sizes, and a scrollable body when content exceeds the viewport.
Import
use PUI
# or
import PUI.Dialog
Basic Usage
Dialogs require an id and can be shown/hidden programmatically:
<.button phx-click={PUI.Dialog.show_dialog("my-dialog")}>
Open Dialog
</.button>
<.dialog id="my-dialog" title="Dialog Title">
<p class="text-muted-foreground">
This is the dialog content.
</p>
<:footer>
<div class="flex justify-end gap-2">
<.button variant="outline"
phx-click={PUI.Dialog.hide_dialog("my-dialog")}>
Cancel
</.button>
<.button>Confirm</.button>
</div>
</:footer>
</.dialog>
Basic Dialog Demo
This is a demonstration of the PUI dialog component.
Dialogs are useful for confirmations, forms, and complex interactions that require user attention.
Title and Close Button
Use title to render a built-in heading and show_close to control the header action:
<.dialog id="profile-dialog" title="Edit profile">
<p>Update your account details.</p>
<:footer>
<div class="flex justify-end gap-2">
<.button variant="outline">Cancel</.button>
<.button>Save</.button>
</div>
</:footer>
</.dialog>
<.dialog id="checkout-dialog" title="Review order" show_close={false}>
<p>Disable the close button when you need a custom action row.</p>
<:footer>
<div class="flex justify-end gap-2">
<.button variant="outline">Back</.button>
<.button>Continue</.button>
</div>
</:footer>
</.dialog>
Scrollable Body and Fixed Footer
The default dialog keeps the title and footer visible while the main content scrolls automatically:
<.dialog id="activity-dialog" title="Recent activity" size="lg">
<div class="space-y-4">
<p :for={index <- 1..12}>
Activity #{index}: This content scrolls inside the dialog body.
</p>
</div>
<:footer>
<div class="flex justify-end gap-2">
<.button variant="outline">Close</.button>
<.button>Save changes</.button>
</div>
</:footer>
</.dialog>
Scrollable Dialog Demo
Activity #1: The body scrolls while the title and footer stay visible.
Activity #2: The body scrolls while the title and footer stay visible.
Activity #3: The body scrolls while the title and footer stay visible.
Activity #4: The body scrolls while the title and footer stay visible.
Activity #5: The body scrolls while the title and footer stay visible.
Activity #6: The body scrolls while the title and footer stay visible.
Activity #7: The body scrolls while the title and footer stay visible.
Activity #8: The body scrolls while the title and footer stay visible.
Activity #9: The body scrolls while the title and footer stay visible.
Activity #10: The body scrolls while the title and footer stay visible.
Activity #11: The body scrolls while the title and footer stay visible.
Activity #12: The body scrolls while the title and footer stay visible.
Activity #13: The body scrolls while the title and footer stay visible.
Activity #14: The body scrolls while the title and footer stay visible.
Activity #15: The body scrolls while the title and footer stay visible.
Activity #16: The body scrolls while the title and footer stay visible.
Activity #17: The body scrolls while the title and footer stay visible.
Activity #18: The body scrolls while the title and footer stay visible.
Activity #19: The body scrolls while the title and footer stay visible.
Activity #20: The body scrolls while the title and footer stay visible.
Activity #21: The body scrolls while the title and footer stay visible.
Activity #22: The body scrolls while the title and footer stay visible.
Activity #23: The body scrolls while the title and footer stay visible.
Activity #24: The body scrolls while the title and footer stay visible.
Activity #25: The body scrolls while the title and footer stay visible.
Activity #26: The body scrolls while the title and footer stay visible.
Activity #27: The body scrolls while the title and footer stay visible.
Activity #28: The body scrolls while the title and footer stay visible.
Activity #29: The body scrolls while the title and footer stay visible.
Activity #30: The body scrolls while the title and footer stay visible.
Server-Controlled
Control dialog visibility from the server with the show attribute:
<.dialog id="server-dialog" show={@show_dialog}>
<p>This dialog is controlled by server state.</p>
<:footer>
<div class="flex justify-end gap-2">
<.button variant="outline" phx-click="close">Cancel</.button>
<.button>Continue</.button>
</div>
</:footer>
</.dialog>
def handle_event("open", _, socket) do
{:noreply, assign(socket, show_dialog: true)}
end
Sizes
Dialogs come in four sizes. For custom widths, set size="" and use class:
<.dialog id="sm-dialog" size="sm">...</.dialog>
<.dialog id="md-dialog" size="md">...</.dialog> <!-- default -->
<.dialog id="lg-dialog" size="lg">...</.dialog>
<.dialog id="xl-dialog" size="xl">...</.dialog>
<!-- Custom width via class -->
<.dialog id="wide-dialog" size="" class="max-w-[80vw] max-h-[80vh]">...</.dialog>
Dialog Sizes Demo
This is a small dialog.
This is a large dialog with more room for content.
This is an extra large dialog for complex content.
Custom Sizing
Use the class attribute with size="" to take full control over dialog dimensions:
<.dialog id="wide-dialog" size="" class="max-w-[80vw] max-h-[80vh]">
<:trigger>
<.button>Open wide dialog</.button>
</:trigger>
<p>This dialog takes 80% of the viewport.</p>
<:footer>
<.button variant="outline">Close</.button>
</:footer>
</.dialog>
Custom Size Demo
Customer Details
This dialog uses
class="max-w-[80vw] max-h-[80vh]"
to
override the default size and span 80% of the viewport in both dimensions.
Recent Orders
Alert Dialog
Alert dialogs require explicit user action and cannot be dismissed by clicking the backdrop:
<.dialog id="alert-dialog" title="Delete Account?" alert={true}>
<p class="mt-2">This action cannot be undone.</p>
<:footer>
<div class="flex justify-end gap-2">
<.button variant="outline"
phx-click={PUI.Dialog.hide_dialog("alert-dialog")}>
Cancel
</.button>
<.button variant="destructive" phx-click="delete_account">
Delete
</.button>
</div>
</:footer>
</.dialog>
Alert Dialog Demo
This action cannot be undone. This will permanently delete the item.
With Trigger Slot
Use the trigger slot for inline trigger buttons:
<.dialog id="trigger-dialog">
<:trigger>
<.button>Open</.button>
</:trigger>
<p>Dialog with trigger slot.</p>
</.dialog>
Unstyled / Headless
<.dialog id="headless" variant="unstyled">
<:content>
<div class="my-custom-dialog-panel">
Custom styled dialog content
</div>
</:content>
</.dialog>
API Reference
Attributes
| Name | Type | Default | Description |
|---|---|---|---|
id |
string |
required | Unique identifier for the dialog |
show |
boolean |
false |
Server-controlled visibility |
alert |
boolean |
false |
Alert dialog mode (no backdrop dismiss) |
size |
string |
"md" |
Dialog size: "sm", "md", "lg", "xl". Set to "" for custom sizing via class |
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 |
variant |
string |
"default" |
"default" or "unstyled" |
class |
string |
"" |
Additional CSS classes applied to the content container. Use with size="" for full custom sizing (e.g. max-w-[80vw] max-h-[80vh]) |
Slots
| Name | Required | Description |
|---|---|---|
trigger |
— | Inline trigger element |
footer |
— | Optional fixed footer for actions in the default layout |
content |
— | Override the entire content container |
inner_block |
— | Main dialog body content |