PhiaUi.Components.Dialog (phia_ui v0.1.17)

Copy Markdown View Source

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

FunctionPurpose
dialog/1Hook anchor, outer container
dialog_trigger/1Opens the dialog via JS.remove_class
dialog_content/1Overlay + panel (hidden by default)
dialog_header/1Title + 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/1Action row (close button, confirmations)
dialog_close/1Closes 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

dialog(assigns)

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 to nil.

Slots

  • inner_block (required)

dialog_close(assigns)

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 to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

dialog_content(assigns)

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

ValueMax 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 to true.

  • scrollable (:boolean) - Add overflow-y-auto to the panel for content that may overflow the viewport. Defaults to false.

  • full_screen_mobile (:boolean) - When true, the dialog panel fills the entire screen on mobile viewports (fixed inset-0 rounded-none) and reverts to its normal centered behavior on sm: 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 to nil.

Slots

  • inner_block (required)

dialog_description(assigns)

Supporting text below the title. Set :id to "{dialog-id}-description" for ARIA linkage.

Attributes

  • id (:string) - Defaults to nil.
  • class (:string) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

dialog_footer(assigns)

Action row at the bottom of the dialog for confirmation and close buttons.

Renders a <div> with flex flex-col-reverse sm:flex-row sm:justify-end sm:space-x-2.

Responsive stacking behaviour:

  • On mobile (< sm): buttons stack vertically in reverse DOM order, so the primary action (last in the template) appears visually at the top — thumb-friendly and consistent with mobile modal conventions.
  • On sm+ screens: buttons arrange horizontally, aligned to the right, with space-x-2 (8px) gaps.

Place the cancel/close action first in the template and the primary (submit/confirm) action last. The CSS reversal puts the primary action at the top on mobile automatically:

Example

<.dialog_footer>
  <%!-- Cancel appears second on desktop, first (top) on mobile --%>
  <.dialog_close for="confirm-delete">Cancel</.dialog_close>
  <%!-- Primary action appears first on desktop (rightmost), top on mobile --%>
  <.button variant={:destructive} phx-click="delete_item">
    Delete
  </.button>
</.dialog_footer>

Attributes

  • class (:string) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

dialog_header(assigns)

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 to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

dialog_title(assigns)

Dialog heading (<h2>). Set :id to "{dialog-id}-title" for ARIA linkage.

Attributes

  • id (:string) - Defaults to nil.
  • class (:string) - Defaults to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)

dialog_trigger(assigns)

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 to nil.
  • Global attributes are accepted.

Slots

  • inner_block (required)