Dialog
Modal window built on native <dialog> + showModal().
Focus trap, Escape key, and backdrop are all browser-native. Animated with @starting-style.
Form Dialog
Dialog with form inputs. Click "Edit Profile" to open.
<button class="btn" data-variant="default"
data-dialog-trigger="edit-dialog"
aria-haspopup="dialog">
Edit Profile
</button>
<dialog id="edit-dialog" class="dialog"
role="dialog" aria-modal="true"
aria-labelledby="edit-title">
<div class="dialog-content">
<div class="dialog-header">
<h2 class="dialog-title" id="edit-title">Edit Profile</h2>
<p class="dialog-description">Make changes to your profile here.</p>
</div>
<div class="dialog-body">
<div style="display:flex;flex-direction:column;gap:1rem;">
<div>
<label class="label" for="name">Name</label>
<input class="input" id="name" type="text" value="Cody Lindley">
</div>
<div>
<label class="label" for="username">Username</label>
<input class="input" id="username" type="text" value="@codylindley">
</div>
</div>
</div>
<div class="dialog-footer">
<button class="btn" data-variant="outline" data-dialog-close>Cancel</button>
<button class="btn" data-variant="default" data-dialog-close>Save changes</button>
</div>
</div>
</dialog>
Confirmation
Destructive action confirmation. Click "Confirm Delete" to open.
<button class="btn" data-variant="outline"
data-dialog-trigger="confirm-dialog"
aria-haspopup="dialog">
Confirm Delete
</button>
<dialog id="confirm-dialog" class="dialog"
role="dialog" aria-modal="true"
aria-labelledby="confirm-title">
<div class="dialog-content">
<div class="dialog-header">
<h2 class="dialog-title" id="confirm-title">Delete account?</h2>
<p class="dialog-description">This action cannot be undone.
This will permanently delete your account.</p>
</div>
<div class="dialog-footer">
<button class="btn" data-variant="outline"
data-dialog-close>Cancel</button>
<button class="btn" data-variant="destructive"
data-dialog-close>Delete account</button>
</div>
</div>
</dialog>
CSS view file
/* -- Dialog component ------------------------------------------ */
@layer components {
dialog.dialog {
border: none;
border-radius: var(--radius-xl);
background-color: var(--popover);
color: var(--popover-foreground);
padding: 0;
margin: auto;
position: fixed;
inset: 0;
max-width: 28rem;
width: calc(100vw - 2rem);
max-height: calc(100vh - 2rem);
box-shadow: 0 25px 80px oklch(0 0 0 / 0.25), 0 0 0 1px var(--border);
opacity: 0;
transform: translateY(-0.5rem) scale(0.98);
transition: opacity 200ms ease, transform 200ms ease, display 200ms allow-discrete;
&[open] {
opacity: 1;
transform: translateY(0) scale(1);
}
/* -- Sizes ------------------------------------------------ */
&[data-size="sm"] { max-width: 24rem; }
&[data-size="lg"] { max-width: 32rem; }
&[data-size="xl"] { max-width: 40rem; }
&[data-size="full"] { max-width: calc(100vw - 2rem); }
/* -- Backdrop --------------------------------------------- */
&::backdrop {
background: oklch(0 0 0 / 0);
backdrop-filter: blur(0px);
transition: all 200ms ease, display 200ms allow-discrete;
}
&[open]::backdrop {
background: oklch(0 0 0 / 0.45);
backdrop-filter: blur(3px);
}
}
@starting-style {
dialog.dialog[open] {
opacity: 0;
transform: translateY(-0.5rem) scale(0.98);
}
dialog.dialog[open]::backdrop {
background: oklch(0 0 0 / 0);
backdrop-filter: blur(0px);
}
}
/* -- Content sections ------------------------------------- */
.dialog-content { padding: 1.5rem; }
.dialog-header { margin-bottom: 1rem; }
.dialog-title { font-size: 1.0625rem; font-weight: 600; margin: 0 0 0.375rem; letter-spacing: -0.01em; }
.dialog-description { font-size: 0.875rem; color: var(--muted-foreground); margin: 0; line-height: 1.6; }
.dialog-body { margin-top: 1rem; }
.dialog-footer { display: flex; justify-content: flex-end; gap: 0.5rem; margin-top: 1.5rem; }
}
JavaScript view file
Wire triggers via data-dialog-trigger, close buttons via data-dialog-close, and backdrop click. Focus returns to trigger on close.
// -- Dialog ---------------------------------------------------
// Wires [data-dialog-trigger] buttons to <dialog> elements.
function init() {
document.querySelectorAll('[data-dialog-trigger]:not([data-init])').forEach((trigger) => {
trigger.dataset.init = '';
const dialog = document.getElementById(trigger.dataset.dialogTrigger);
if (!dialog) return;
trigger.addEventListener('click', () => {
dialog._trigger = trigger;
dialog.showModal();
});
});
document.querySelectorAll('dialog:not(.alert-dialog):not(.sheet):not([data-init])').forEach((dialog) => {
dialog.dataset.init = '';
dialog.addEventListener('click', (e) => {
if (e.target === dialog) dialog.close();
});
dialog.querySelectorAll('[data-dialog-close]').forEach((btn) => {
btn.addEventListener('click', () => { dialog.close(); });
});
dialog.addEventListener('close', () => {
if (dialog._trigger) dialog._trigger.focus();
});
});
}
init();
new MutationObserver(init).observe(document, { childList: true, subtree: true });