Skip to content
Docs

Dialog

A window overlaid on the primary window, rendering the content underneath inert.

A window overlaid on either the primary window or another dialog window, rendering the content underneath inert. Headless dialog primitive—pair with your own styles (or Themes Dialog) for the visual layer.

<Dialog.root id="dialog-primitive">
  <Dialog.trigger
    id="dialog-trigger"
    class="DemoButton violet"
  >
    Edit profile
  </Dialog.trigger>
  <Dialog.portal id="dialog-portal">
    <Dialog.overlay class="DemoDialogOverlay" />
    <Dialog.content
      id="dialog-content"
      class="DemoDialogContent"
    >
      <Dialog.title class="DemoDialogTitle">Edit profile</Dialog.title>
      <Dialog.description class="DemoDialogDescription">
        Make changes to your profile here. Click save when you're done.
      </Dialog.description>
      <fieldset class="DemoFieldset">
        <label
          class="DemoLabel"
          for="dialog-name"
        >
          Name
        </label>
        <input
          class="DemoInput"
          id="dialog-name"
          value="Pedro Duarte"
        />
      </fieldset>
      <fieldset class="DemoFieldset">
        <label
          class="DemoLabel"
          for="dialog-username"
        >
          Username
        </label>
        <input
          class="DemoInput"
          id="dialog-username"
          value="@peduarte"
        />
      </fieldset>
      <div style="display: flex; margin-top: 25px; justify-content: flex-end;">
        <Dialog.close class="DemoButton green">Save changes</Dialog.close>
      </div>
      <Dialog.close
        class="DemoIconButton"
        aria-label="Close"
      >
        ×
      </Dialog.close>
    </Dialog.content>
  </Dialog.portal>
</Dialog.root>
Supports modal and non-modal modes.
Focus is trapped within modal dialogs.
Can be controlled or uncontrolled.
Manages screen reader announcements with Title and Description.
Escape closes the dialog automatically.

Anatomy

Import all parts and piece them together.

<Dialog.root>
  <Dialog.trigger />
  <Dialog.portal>
    <Dialog.overlay />
    <Dialog.content>
      <Dialog.title />
      <Dialog.description />
      <Dialog.close />
    </Dialog.content>
  </Dialog.portal>
</Dialog.root>

Anatomy

Root
Contains all dialog parts.
Trigger
The button that opens the dialog.
Portal
Portals overlay and content into the document body.
Overlay
Dimmed layer behind the content.
Content
Contains title, description, and actions.
Title
Accessible title announced on open.
Description
Optional accessible description.
Close
Control that closes the dialog.

API Reference

Root

Contains all the parts of a dialog.

Use open with on_open_change for controlled open state in LiveView:

<Dialog.root id="profile-dialog" open={@open} on_open_change="dialog_open_change">
  …
</Dialog.root>
def handle_event("dialog_open_change", %{"open" => open}, socket) do
  {:noreply, assign(socket, :open, open)}
end
Prop Type Default Description
default_open boolean false Initial open state when uncontrolled.
id string Unique id for the dialog root.
modal boolean true When true, outside interaction is disabled and focus is trapped.
on_open_change string nil LiveView event name pushed when open state changes.
open boolean false Controlled open state.
default_open Initial open state when uncontrolled.
Type boolean Default false
id Unique id for the dialog root.
Type string Default
modal When true, outside interaction is disabled and focus is trapped.
Type boolean Default true
on_open_change LiveView event name pushed when open state changes.
Type string Default nil
open Controlled open state.
Type boolean Default false

Data attributes

Attribute Values Description
[data-state] open | closed Present on the root reflecting open state.

Trigger

The button that opens the dialog.

Prop Type Default Description
id string nil Optional id for the trigger button.
id Optional id for the trigger button.
Type string Default nil

Data attributes

Attribute Values Description
[data-state] open | closed Reflects whether the dialog is open.

Portal

Portals your overlay and content parts into the target (default body).

Prop Type Default Description
id string Portal id required by Phoenix.Component.portal.
target string "body" CSS selector for the portal target (default body).
id Portal id required by Phoenix.Component.portal.
Type string Default
target CSS selector for the portal target (default body).
Type string Default "body"

Overlay

A layer that covers the inert portion of the view when the dialog is open.

Prop Type Default Description
id string nil
id
Type string Default nil

Data attributes

Attribute Values Description
[data-state] open | closed Reflects whether the dialog is open.

Content

Contains content to be rendered in the open dialog.

Prop Type Default Description
id string nil
id
Type string Default nil

Data attributes

Attribute Values Description
[data-state] open | closed Reflects whether the dialog is open.

Title

An accessible title announced when the dialog is opened. Hide with Visually Hidden if needed—still provide a title for accessibility.

Prop Type Default Description
id string nil
id
Type string Default nil

Description

An optional accessible description announced when the dialog is opened.

Prop Type Default Description
id string nil
id
Type string Default nil

Close

The button that closes the dialog.

Prop Type Default Description
id string nil
id
Type string Default nil

Examples

Close after async action

Close from LiveView by setting controlled open={false} after a successful event, or use Dialog.close inside the content for immediate dismiss.

<Dialog.root id="async-dialog" open={@open} on_open_change="dialog_open_change">
  <Dialog.trigger id="async-dialog-trigger" class="DemoButton">Edit</Dialog.trigger>
  <Dialog.portal id="async-dialog-portal">
    <Dialog.overlay class="DemoDialogOverlay" />
    <Dialog.content id="async-dialog-content" class="DemoDialogContent">
      <Dialog.title>Edit</Dialog.title>
      <form phx-submit="save">
        …
        <Dialog.close class="DemoButton">Cancel</Dialog.close>
        <button type="submit" class="DemoButton green">Save</button>
      </form>
    </Dialog.content>
  </Dialog.portal>
</Dialog.root>

Scrollable overlay

Make the overlay the scroll container so long content scrolls within the viewport:

.DemoDialogOverlay {
  overflow-y: auto;
  display: grid;
  place-items: center;
}

Custom portal target

Portal into a specific container via target:

<Dialog.portal id="dialog-portal" target="#app-portal-root">
  …
</Dialog.portal>

Accessibility

Adheres to the Dialog WAI-ARIA design pattern.

Keyboard Interactions

Key Description
Space Opens/closes the dialog when focus is on the trigger or close control.
Enter Opens/closes the dialog when focus is on the trigger or close control.
Tab Moves focus to the next focusable element inside the dialog.
Shift + Tab Moves focus to the previous focusable element inside the dialog.
Escape Closes the dialog and returns focus to the trigger.

Custom APIs

Create your own API by wrapping the primitive parts into a friendlier abstraction for your design system.

Abstract the overlay and the close button

This example abstracts Dialog.overlay and Dialog.close into a reusable confirm dialog.

Usage

<.confirm_dialog id="delete-confirm" title="Are you sure?">
  This action cannot be undone.
</.confirm_dialog>

Implementation

def confirm_dialog(assigns) do
  ~H"""
  <Dialog.root id={@id}>
    <Dialog.trigger id={"#{@id}-trigger"} class="DemoButton">
      {render_slot(@trigger) || "Open"}
    </Dialog.trigger>
    <Dialog.portal id={"#{@id}-portal"}>
      <Dialog.overlay class="DemoDialogOverlay" />
      <Dialog.content id={"#{@id}-content"} class="DemoDialogContent">
        <Dialog.title class="DemoDialogTitle">{@title}</Dialog.title>
        <Dialog.description class="DemoDialogDescription">
          {render_slot(@inner_block)}
        </Dialog.description>
        <Dialog.close class="DemoButton">Cancel</Dialog.close>
        <Dialog.close class="DemoButton violet">Confirm</Dialog.close>
      </Dialog.content>
    </Dialog.portal>
  </Dialog.root>
  """
end