Skip to content

Dialog

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

<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" 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-secondary"
    onclick="this.closest('dialog').close()">
      Cancel
    </button>
    <button class="af-btn af-is-danger"
    onclick="this.closest('dialog').close()">
      Delete
    </button>
  </div>
</dialog>
<button class="af-btn"
onclick="document.getElementById('my-dialog').showModal()">
  Open Dialog
</button>

Confirm action

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

  • 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-bodyMain content area
af-dialog-footerFooter row. Flexbox, end-aligned.
<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" 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-secondary" formmethod="dialog">
        Cancel
      </button>
      <button class="af-btn">
        Save
      </button>
    </div>
  </form>
</dialog>
<button class="af-btn"
onclick="document.getElementById('form-dialog').showModal()">
  Edit Profile
</button>

Using <form method="dialog"> lets any submit button close the dialog automatically.

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