Dialog
Modal dialog using the native HTML <dialog> element. No JavaScript dependencies required.
Basic dialog
Section titled “Basic dialog”Click the button to open a real modal. ESC, the close control, Cancel, Confirm, or the backdrop (if wired) dismiss it.
<button class="af-btn"
onclick="document.getElementById('my-dialog').showModal()">Open Dialog</button>
<dialog class="af-dialog" id="my-dialog">
<div class="af-dialog-header">
<h2 class="af-dialog-title">Confirm action</h2>
<button class="af-btn af-btn-icon af-is-sm af-is-outline af-is-secondary" aria-label="Close"
onclick="this.closest('dialog').close()">×</button>
</div>
<div class="af-dialog-body">
<p>Are you sure you want to continue? This action cannot be undone.</p>
</div>
<div class="af-dialog-footer">
<button class="af-btn af-is-outline af-is-secondary"
onclick="this.closest('dialog').close()">Cancel</button>
<button class="af-btn"
onclick="this.closest('dialog').close()">Confirm</button>
</div>
</dialog>Why native <dialog>?
Section titled “Why native <dialog>?”showModal()marks the rest of the page as inert.- ESC key closes the dialog automatically.
- Body scroll lock while the modal is open.
::backdroppseudo-element. No extra markup for the overlay.
Note: Native <dialog> provides basic focus containment via
page inertness, but focus trapping and focus restoration are not fully reliable across all
browsers and screen readers. For production apps, supplement with a focus trap library (e.g.
focus-trap).
Open with showModal(). Close with close().
const dialog = document.getElementById('my-dialog');
dialog.showModal();
dialog.close();
if (dialog.open) {/* dialog is visible */}| Class | Purpose |
|---|---|
af-dialog | The <dialog> element itself |
af-dialog-header | Header row. Flexbox, space-between. |
af-dialog-title | Title text inside the header |
af-dialog-body | Scrollable content area |
af-dialog-footer | Footer row. Inline actions, end-aligned. |
Use a quiet close control in the header: af-btn af-btn-icon af-is-sm af-is-outline af-is-secondary. Pair footer actions as outline secondary + primary.
Scrollable body
Section titled “Scrollable body”Header and footer stay fixed. Long content scrolls inside af-dialog-body.
<button class="af-btn"
onclick="document.getElementById('scroll-dialog').showModal()">Open release notes</button>
<dialog class="af-dialog" id="scroll-dialog">
<div class="af-dialog-header">
<h2 class="af-dialog-title">Release notes</h2>
<button class="af-btn af-btn-icon af-is-sm af-is-outline af-is-secondary" aria-label="Close"
onclick="this.closest('dialog').close()">×</button>
</div>
<div class="af-dialog-body">
<div class="af-stack af-gap-md">
<p>Thanks for updating. Here's what's new in this release.</p>
<h3 class="af-text-h5">Highlights</h3>
<ul>
<li>Faster page loads with leaner CSS delivery</li>
<li>Improved keyboard focus styles across controls</li>
<li>New dialog layout with a scrollable body region</li>
<li>Outline secondary buttons for quieter dismiss actions</li>
</ul>
<h3 class="af-text-h5">Fixes</h3>
<ul>
<li>Corrected vertical alignment of inline footer actions</li>
<li>Stabilized select and input focus rings in dark mode</li>
<li>Prevented drawer panels from inheriting dialog width tokens</li>
<li>Fixed form fields stacking too tightly in compact layouts</li>
</ul>
<h3 class="af-text-h5">Notes</h3>
<p>If you maintain a custom theme, regenerate your allowlist after pulling token changes. Existing semantic color overrides continue to work.</p>
<p>
Long-form dialog content should live in
<code>af-dialog-body</code>
so the header and footer remain pinned while the middle section scrolls.
</p>
<p>Prefer short confirmations when possible. Reach for a scrollable body when the user needs to review terms, changelog details, or multi-step context before continuing.</p>
</div>
</div>
<div class="af-dialog-footer">
<button class="af-btn af-is-outline af-is-secondary"
onclick="this.closest('dialog').close()">Dismiss</button>
<button class="af-btn"
onclick="this.closest('dialog').close()">Got it</button>
</div>
</dialog>Dialog with form
Section titled “Dialog with form”<button class="af-btn"
onclick="document.getElementById('form-dialog').showModal()">Edit Profile</button>
<dialog class="af-dialog" id="form-dialog">
<form method="dialog">
<div class="af-dialog-header">
<h2 class="af-dialog-title">Edit profile</h2>
<button class="af-btn af-btn-icon af-is-sm af-is-outline af-is-secondary" aria-label="Close"
formmethod="dialog">×</button>
</div>
<div class="af-dialog-body">
<div class="af-stack af-gap-md">
<div class="af-field">
<label for="name" class="af-label">Name</label>
<input id="name" class="af-input" type="text" value="Iain Todd">
</div>
<div class="af-field">
<label for="email" class="af-label">Email</label>
<input id="email" class="af-input" type="email" value="iain@example.com">
</div>
</div>
</div>
<div class="af-dialog-footer">
<button class="af-btn af-is-outline af-is-secondary" formmethod="dialog">Cancel</button>
<button class="af-btn">Save</button>
</div>
</form>
</dialog>Using <form method="dialog"> lets any submit button close the dialog automatically.
Default width is 26rem (max 90vw). Override with af-is-sm (20rem), af-is-lg (40rem), or af-is-xl (56rem). Set --af-dialog-width on the dialog for a custom size.
Widths are 20 / 26 / 40 / 56rem (capped at 90vw).
af-is-sm
Default
af-is-lg
<dialog class="af-dialog af-is-sm">…</dialog>
<dialog class="af-dialog">…</dialog>
<dialog class="af-dialog af-is-lg">…</dialog>
<dialog class="af-dialog af-is-xl">…</dialog>Closing on backdrop click
Section titled “Closing on backdrop click”The native <dialog> doesn’t close on backdrop click by default. Add this if you want it:
dialog.addEventListener('click', (e) => {
if (e.target === dialog) dialog.close();
});Accessibility
Section titled “Accessibility”- Use
<dialog>element. Not a<div>withrole="dialog". - Provide a visible title via
af-dialog-title. - Add
aria-labeloraria-labelledbyif the title is not visible. - Close buttons need
aria-label="Close".