Skip to content

Dialog

A modal popup built on the native dialog element with trigger wiring, ARIA sync, and optional backdrop dismiss.

<pe-dialog>
<button type="button" class="Trigger" data-dialog-trigger>Open dialog</button>

<dialog class="Dialog">
  <header class="Header">
    <h2 class="Title" data-dialog-title>Example dialog</h2>
    <button type="button" class="Close" data-dialog-close>Close</button>
  </header>
  <p class="Description" data-dialog-description>
    A native modal dialog enhanced with trigger wiring and ARIA sync.
  </p>
</dialog>
</pe-dialog>

Use Dialog for modal tasks and confirmations. Add data-dialog-dismiss only when backdrop interaction should close it; use native form method="dialog" for simple confirm/cancel actions.

Part Selector Role
Host <pe-dialog> Lifecycle host for one <dialog>
Dialog <dialog> Native modal surface
Trigger [data-dialog-trigger] Opens the dialog. Empty value = internal trigger
Title [data-dialog-title] Sets aria-labelledby when no explicit label exists
Description [data-dialog-description] Sets aria-describedby
Close [data-dialog-close] Closes the dialog on click
Dismiss data-dialog-dismiss on <dialog> Opt-in backdrop pointer dismiss

Use role="alertdialog" for a short, urgent message that requires a response.

<button type="button" class="Trigger" data-dialog-trigger="delete-dialog">
Delete item
</button>

<pe-dialog id="delete-dialog">
<dialog class="Dialog" role="alertdialog" aria-modal="true">
  <h2 class="Title" data-dialog-title>Delete this item?</h2>
  <p class="Description" data-dialog-description>
    This action cannot be undone.
  </p>

  <form class="Actions" method="dialog">
    <button value="cancel" autofocus>Cancel</button>
    <button class="ButtonDanger" value="confirm">Delete</button>
  </form>
</dialog>
</pe-dialog>

<p id="delete-result" class="Status" role="status"></p>
  • Provide a name and description.
  • Do not add data-dialog-dismiss.
  • Put autofocus on the least destructive action.
  • Perform the action only after an explicit form confirmation.
const host = document.querySelector("#delete-dialog");
host.addEventListener("pe-dialog:close", (event) => {
const { dialog, reason } = event.detail;
if (reason === "form" && dialog.returnValue === "confirm") {
deleteItem();
}
});
NameTypeDescription
isOpenboolean (readonly)Reads whether the dialog is currently open.
open(trigger?)voidOpens the dialog with showModal(). Pass the activating trigger when opening programmatically so focus restoration and events include it.
close(returnValue?)voidCloses the dialog. Forwards an optional returnValue to HTMLDialogElement.close().
toggle(trigger?)voidOpens if closed, closes if open.
NameDescription
data-dialog-triggerMarks a trigger. Empty value targets an internal dialog. A host id string targets an external dialog.
data-dialog-closeCloses the dialog when activated.
data-dialog-titlePrimary label source for aria-labelledby.
data-dialog-descriptionDescription source for aria-describedby.
data-dialog-dismissOn <dialog>: enable backdrop pointer dismiss.
data-stateOn host/dialog and triggers: "open" or "closed". With shared triggers, only the active trigger is open.
NameDescription
pe-dialog:openFired after the dialog opens. detail includes dialog, optional trigger, and reason.
pe-dialog:closeFired after the dialog closes. detail includes dialog, optional trigger, and reason.
pe-dialog:cancelFired after a native cancel request. detail includes dialog, optional trigger, reason, and whether the native event was prevented.

Close reasons in detail.reason: trigger, close-control, form, cancel, dismiss, programmatic, native.

A prevented native cancel request fires pe-dialog:cancel with detail.defaultPrevented set to true, keeps the dialog open, and does not fire pe-dialog:close.

Dialogs can be nested by placing <pe-dialog> inside another dialog surface. Focus restoration and dismiss behavior follow the topmost open dialog.

<button type="button" class="Trigger" data-dialog-trigger="parent-dialog">
Open parent dialog
</button>

<pe-dialog id="parent-dialog">
<dialog class="Dialog" data-dialog-dismiss>
  <header class="Header">
    <h2 class="Title" data-dialog-title>Parent dialog</h2>
    <button type="button" class="Close" data-dialog-close>Close parent</button>
  </header>
  <p class="Description" data-dialog-description>
    Open a child dialog without closing this parent dialog.
  </p>
  <button type="button" class="Trigger" data-dialog-trigger="child-dialog">
    Open child dialog
  </button>

  <pe-dialog id="child-dialog">
    <dialog class="Dialog">
      <header class="Header">
        <h2 class="Title" data-dialog-title>Child dialog</h2>
        <button type="button" class="Close" data-dialog-close>Close child</button>
      </header>
      <p class="Description" data-dialog-description>
        Closing this dialog restores focus to its trigger in the parent dialog.
      </p>
    </dialog>
  </pe-dialog>
</dialog>
</pe-dialog>

Nested dialogs expose these styling hooks on <dialog>:

Name Description
data-nested Present when the dialog is nested within another dialog.
data-nested-dialog-open Present when this open dialog has nested open dialogs inside it.
--nested-dialogs Nesting depth while open (0 for the root dialog, 1 for a child, and so on).