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>.Trigger {
min-height: 2rem;
padding: 0 0.75rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
@media (prefers-color-scheme: dark) {
border-color: white;
}
@media (hover: hover) {
&:hover {
background: oklch(92% 0 0);
@media (prefers-color-scheme: dark) {
background: oklch(26.9% 0 0);
}
}
}
}
pe-dialog {
display: contents;
}
.Dialog {
width: min(20rem, calc(100vw - 2rem));
margin: 0;
padding: 1rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: oklch(97% 0 0);
color: oklch(14.5% 0 0);
@media (prefers-color-scheme: dark) {
border-color: white;
background: oklch(20% 0 0);
color: white;
}
&::backdrop {
background: oklch(0% 0 0 / 45%);
@media (prefers-color-scheme: dark) {
background: oklch(0% 0 0 / 65%);
}
}
}
.Dialog .Header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.Dialog .Title {
margin: 0;
font-size: 1rem;
font-weight: 500;
line-height: 1.25rem;
}
.Dialog .Description {
margin: 0;
line-height: 1.5;
}
.Dialog .Close {
min-height: 2rem;
padding: 0 0.5rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
@media (prefers-color-scheme: dark) {
border-color: white;
}
@media (hover: hover) {
&:hover {
background: oklch(92% 0 0);
@media (prefers-color-scheme: dark) {
background: oklch(26.9% 0 0);
}
}
}
}
.Dialog .Actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
.Dialog .Actions button {
min-height: 2rem;
padding: 0 0.75rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
@media (prefers-color-scheme: dark) {
border-color: white;
}
}
.Dialog .ButtonDanger {
border-color: oklch(14.5% 0 0);
background: oklch(14.5% 0 0);
color: white;
@media (prefers-color-scheme: dark) {
border-color: white;
background: white;
color: oklch(14.5% 0 0);
}
}
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.
Anatomy
Section titled “Anatomy”| 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 |
Alert dialog
Section titled “Alert dialog”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>.Row {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
}
.Status {
margin: 0;
}
.Trigger {
min-height: 2rem;
padding: 0 0.75rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
@media (prefers-color-scheme: dark) {
border-color: white;
}
@media (hover: hover) {
&:hover {
background: oklch(92% 0 0);
@media (prefers-color-scheme: dark) {
background: oklch(26.9% 0 0);
}
}
}
}
pe-dialog {
display: contents;
}
.Dialog {
width: min(20rem, calc(100vw - 2rem));
margin: 0;
padding: 1rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: oklch(97% 0 0);
color: oklch(14.5% 0 0);
@media (prefers-color-scheme: dark) {
border-color: white;
background: oklch(20% 0 0);
color: white;
}
&::backdrop {
background: oklch(0% 0 0 / 45%);
@media (prefers-color-scheme: dark) {
background: oklch(0% 0 0 / 65%);
}
}
}
.Dialog .Header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.Dialog .Title {
margin: 0;
font-size: 1rem;
font-weight: 500;
line-height: 1.25rem;
}
.Dialog .Description {
margin: 0;
line-height: 1.5;
}
.Dialog .Close {
min-height: 2rem;
padding: 0 0.5rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
@media (prefers-color-scheme: dark) {
border-color: white;
}
@media (hover: hover) {
&:hover {
background: oklch(92% 0 0);
@media (prefers-color-scheme: dark) {
background: oklch(26.9% 0 0);
}
}
}
}
.Dialog .Actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
.Dialog .Actions button {
min-height: 2rem;
padding: 0 0.75rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
@media (prefers-color-scheme: dark) {
border-color: white;
}
}
.Dialog .ButtonDanger {
border-color: oklch(14.5% 0 0);
background: oklch(14.5% 0 0);
color: white;
@media (prefers-color-scheme: dark) {
border-color: white;
background: white;
color: oklch(14.5% 0 0);
}
}
- Provide a name and description.
- Do not add
data-dialog-dismiss. - Put
autofocuson 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(); }});API Reference
Section titled “API Reference”Host methods
Section titled “Host methods”| Name | Type | Description |
|---|---|---|
isOpen | boolean (readonly) | Reads whether the dialog is currently open. |
open(trigger?) | void | Opens the dialog with showModal(). Pass the activating trigger when opening programmatically so focus restoration and events include it. |
close(returnValue?) | void | Closes the dialog. Forwards an optional returnValue to HTMLDialogElement.close(). |
toggle(trigger?) | void | Opens if closed, closes if open. |
Data attributes
Section titled “Data attributes”| Name | Description |
|---|---|
data-dialog-trigger | Marks a trigger. Empty value targets an internal dialog. A host id string targets an external dialog. |
data-dialog-close | Closes the dialog when activated. |
data-dialog-title | Primary label source for aria-labelledby. |
data-dialog-description | Description source for aria-describedby. |
data-dialog-dismiss | On <dialog>: enable backdrop pointer dismiss. |
data-state | On host/dialog and triggers: "open" or "closed". With shared triggers, only the active trigger is open. |
Events
Section titled “Events”| Name | Description |
|---|---|
pe-dialog:open | Fired after the dialog opens. detail includes dialog, optional trigger, and reason. |
pe-dialog:close | Fired after the dialog closes. detail includes dialog, optional trigger, and reason. |
pe-dialog:cancel | Fired 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.
Nested dialogs
Section titled “Nested dialogs”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>.Trigger {
min-height: 2rem;
padding: 0 0.75rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
@media (prefers-color-scheme: dark) {
border-color: white;
}
@media (hover: hover) {
&:hover {
background: oklch(92% 0 0);
@media (prefers-color-scheme: dark) {
background: oklch(26.9% 0 0);
}
}
}
}
pe-dialog {
display: contents;
}
.Dialog {
width: min(20rem, calc(100vw - 2rem));
margin: 0;
padding: 1rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: oklch(97% 0 0);
color: oklch(14.5% 0 0);
@media (prefers-color-scheme: dark) {
border-color: white;
background: oklch(20% 0 0);
color: white;
}
&::backdrop {
background: oklch(0% 0 0 / 45%);
@media (prefers-color-scheme: dark) {
background: oklch(0% 0 0 / 65%);
}
}
}
.Dialog .Header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
margin-bottom: 0.75rem;
}
.Dialog .Title {
margin: 0;
font-size: 1rem;
font-weight: 500;
line-height: 1.25rem;
}
.Dialog .Description {
margin: 0;
line-height: 1.5;
}
.Dialog .Close {
min-height: 2rem;
padding: 0 0.5rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
@media (prefers-color-scheme: dark) {
border-color: white;
}
@media (hover: hover) {
&:hover {
background: oklch(92% 0 0);
@media (prefers-color-scheme: dark) {
background: oklch(26.9% 0 0);
}
}
}
}
.Dialog .Actions {
display: flex;
justify-content: flex-end;
gap: 0.5rem;
margin-top: 1rem;
}
.Dialog .Actions button {
min-height: 2rem;
padding: 0 0.75rem;
border: 1px solid oklch(14.5% 0 0);
border-radius: 0;
background: transparent;
color: inherit;
cursor: pointer;
@media (prefers-color-scheme: dark) {
border-color: white;
}
}
.Dialog .ButtonDanger {
border-color: oklch(14.5% 0 0);
background: oklch(14.5% 0 0);
color: white;
@media (prefers-color-scheme: dark) {
border-color: white;
background: white;
color: oklch(14.5% 0 0);
}
}
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). |