Accessible Dialog (Modal) component following the WAI-ARIA Dialog pattern.
Uses Phoenix.LiveView.JS for open/close transitions and the PhiaDialog
JavaScript Hook for focus trap, keyboard navigation, and scroll locking.
Registration in app.js
After running mix phia.add dialog, register the hook in your LiveSocket:
import PhiaDialog from "./phia_hooks/dialog.js"
let liveSocket = new LiveSocket("/live", Socket, {
hooks: { PhiaDialog, ...yourOtherHooks }
})Example
<.dialog id="confirm-delete">
<.dialog_trigger for="confirm-delete">
<.button variant={:destructive}>Delete</.button>
</.dialog_trigger>
<.dialog_content id="confirm-delete">
<.dialog_header>
<.dialog_title id="confirm-delete-title">Delete Item</.dialog_title>
<.dialog_description id="confirm-delete-description">
This action cannot be undone.
</.dialog_description>
</.dialog_header>
<.dialog_footer>
<.dialog_close for="confirm-delete">Cancel</.dialog_close>
<.button variant={:destructive} phx-click="delete">Delete</.button>
</.dialog_footer>
</.dialog_content>
</.dialog>Sub-components
| Function | Purpose |
|---|---|
dialog/1 | Hook anchor, outer container |
dialog_trigger/1 | Opens the dialog via JS.remove_class |
dialog_content/1 | Overlay + panel (hidden by default) |
dialog_header/1 | Title + description layout container |
dialog_title/1 | <h2> heading (set id for ARIA linkage) |
dialog_description/1 | <p> supporting text (set id for ARIA) |
dialog_footer/1 | Action row (close button, confirmations) |
dialog_close/1 | Closes the dialog via JS.add_class |
Summary
Functions
Outer container for the Dialog. Binds the PhiaDialog JavaScript Hook.
Closes the dialog via JS.add_class("hidden") on #dialog-{for}.
The Escape key is also handled by the PhiaDialog JS Hook.
The dialog surface: renders the overlay backdrop and the modal panel.
Supporting text below the title. Set :id to "{dialog-id}-description" for ARIA linkage.
Action row at the bottom of the dialog for confirmation and close buttons.
Layout container for the dialog title and description.
Dialog heading (<h2>). Set :id to "{dialog-id}-title" for ARIA linkage.
Opens the dialog by calling JS.remove_class("hidden") on #dialog-{for}.
Functions
Outer container for the Dialog. Binds the PhiaDialog JavaScript Hook.
Must wrap both dialog_trigger/1 and dialog_content/1.
Attributes
id(:string) (required) - Unique ID used as the hook anchor.class(:string) - Additional CSS classes. Defaults tonil.
Slots
inner_block(required)
Closes the dialog via JS.add_class("hidden") on #dialog-{for}.
The Escape key is also handled by the PhiaDialog JS Hook.
Attributes
for(:string) (required) - ID of the dialog to close (matches dialog/1's :id).class(:string) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)
The dialog surface: renders the overlay backdrop and the modal panel.
Hidden by default. Shown by dialog_trigger/1 via JS.remove_class("hidden").
The outer container id is "dialog-{id}" (prefixed) so that
dialog_trigger/1 and dialog_close/1 can target it.
Size variants
| Value | Max width |
|---|---|
"sm" | max-w-sm |
"default" | max-w-lg (default) |
"lg" | max-w-2xl |
"xl" | max-w-4xl |
"full" | max-w-[calc(100vw-2rem)] |
Attributes
id(:string) (required) - ID matching the parent dialog/1's :id.size(:string) - Width of the dialog panel. Defaults to"default". Must be one of"sm","default","lg","xl", or"full".show_close_button(:boolean) - Render the default X close button in the top-right corner of the panel. Defaults totrue.scrollable(:boolean) - Add overflow-y-auto to the panel for content that may overflow the viewport. Defaults tofalse.full_screen_mobile(:boolean) - Whentrue, the dialog panel fills the entire screen on mobile viewports (fixed inset-0 rounded-none) and reverts to its normal centered behavior onsm:and wider screens (sm:relative sm:inset-auto sm:rounded-lg). Ideal for complex forms or content that benefits from full viewport space on small devices.Defaults to
false.class(:string) - Additional classes for the panel. Defaults tonil.
Slots
inner_block(required)
Supporting text below the title. Set :id to "{dialog-id}-description" for ARIA linkage.
Attributes
id(:string) - Defaults tonil.class(:string) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)
Layout container for the dialog title and description.
Renders a <div> with flex flex-col space-y-1.5 text-center sm:text-left.
On mobile viewports the content is centred; on sm and wider it aligns
to the left, matching the standard modal convention for each form factor.
Wrap dialog_title/1 and dialog_description/1 inside this component.
Both must have their :id set (to "{dialog-id}-title" and
"{dialog-id}-description") so the panel's aria-labelledby and
aria-describedby attributes resolve correctly.
Example
<.dialog_header>
<.dialog_title id="confirm-delete-title">
Delete project?
</.dialog_title>
<.dialog_description id="confirm-delete-description">
All data will be permanently removed. This cannot be undone.
</.dialog_description>
</.dialog_header>Attributes
class(:string) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)
Dialog heading (<h2>). Set :id to "{dialog-id}-title" for ARIA linkage.
Attributes
id(:string) - Defaults tonil.class(:string) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)
Opens the dialog by calling JS.remove_class("hidden") on #dialog-{for}.
Attributes
for(:string) (required) - ID of the dialog to open (matches dialog/1's :id).class(:string) - Defaults tonil.- Global attributes are accepted.
Slots
inner_block(required)