Skip to content

Dialog

Modal dialog using the native HTML <dialog> element. No JavaScript dependencies required.

Click the button to open a real modal. ESC, the close control, Cancel, Confirm, or the backdrop (if wired) dismiss it.

Confirm action

Are you sure you want to continue? This action cannot be undone.

<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>
  • showModal() marks the rest of the page as inert.
  • ESC key closes the dialog automatically.
  • Body scroll lock while the modal is open.
  • ::backdrop pseudo-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 */}
ClassPurpose
af-dialogThe <dialog> element itself
af-dialog-headerHeader row. Flexbox, space-between.
af-dialog-titleTitle text inside the header
af-dialog-bodyScrollable content area
af-dialog-footerFooter 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.

Header and footer stay fixed. Long content scrolls inside af-dialog-body.

Release notes

Thanks for updating. Here's what's new in this release.

Highlights

  • Faster page loads with leaner CSS delivery
  • Improved keyboard focus styles across controls
  • New dialog layout with a scrollable body region
  • Outline secondary buttons for quieter dismiss actions

Fixes

  • Corrected vertical alignment of inline footer actions
  • Stabilized select and input focus rings in dark mode
  • Prevented drawer panels from inheriting dialog width tokens
  • Fixed form fields stacking too tightly in compact layouts

Notes

If you maintain a custom theme, regenerate your allowlist after pulling token changes. Existing semantic color overrides continue to work.

Long-form dialog content should live in af-dialog-body so the header and footer remain pinned while the middle section scrolls.

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.

<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>

Edit profile

<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>

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();
});
  • Use <dialog> element. Not a <div> with role="dialog".
  • Provide a visible title via af-dialog-title.
  • Add aria-label or aria-labelledby if the title is not visible.
  • Close buttons need aria-label="Close".