send.webforms.toRead the docs →

A popup form with the dialog element

Modal libraries exist because building focus trapping, Escape handling and backdrops by hand is miserable. The native dialog element ships all three. This is a complete popup contact form - trigger button, modal, AJAX submit so success shows inside the dialog - in about eighty lines with no dependency.

Copy-paste HTML

index.html
<button type="button" id="open-contact">Contact us</button>

<dialog id="contact-dialog">
  <form id="dialog-form" action="https://send.webforms.to/wft_your_key" method="POST">
    <h2>Send us a message</h2>

    <label for="pc-name">Name</label>
    <input id="pc-name" name="name" type="text" required>

    <label for="pc-email">Email</label>
    <input id="pc-email" name="email" type="email" required>

    <label for="pc-message">Message</label>
    <textarea id="pc-message" name="message" rows="4" required></textarea>

    <div class="dialog-actions">
      <button type="button" id="close-contact">Cancel</button>
      <button type="submit">Send</button>
    </div>
    <p id="dialog-status" role="status" aria-live="polite"></p>
  </form>
</dialog>

<style>
  #contact-dialog {
    border: 1px solid #ddd;
    border-radius: 12px;
    padding: 1.5rem;
    max-width: 26rem;
    width: calc(100% - 2rem);
  }
  #contact-dialog::backdrop { background: rgb(0 0 0 / 0.45); }
  #contact-dialog label { display: block; margin: 0.9rem 0 0.3rem; }
  #contact-dialog input, #contact-dialog textarea {
    width: 100%; box-sizing: border-box; padding: 0.55rem 0.7rem;
    border: 1px solid #ccc; border-radius: 8px; font: inherit;
  }
  .dialog-actions { display: flex; gap: 0.5rem; justify-content: flex-end; margin-top: 1.25rem; }
</style>

<script>
  const dialog = document.getElementById("contact-dialog");
  const dialogForm = document.getElementById("dialog-form");
  const dialogStatus = document.getElementById("dialog-status");

  document.getElementById("open-contact")
    .addEventListener("click", () => dialog.showModal());
  document.getElementById("close-contact")
    .addEventListener("click", () => dialog.close());

  dialogForm.addEventListener("submit", async (e) => {
    e.preventDefault();
    dialogStatus.textContent = "Sending...";

    try {
      const res = await fetch(dialogForm.action, {
        method: "POST",
        body: new FormData(dialogForm),
        headers: { Accept: "application/json" },
      });
      if (res.ok) {
        dialogForm.reset();
        dialogStatus.textContent = "Sent - we'll get back to you soon.";
        setTimeout(() => {
          dialog.close();
          dialogStatus.textContent = "";
        }, 1600);
      } else {
        dialogStatus.textContent = "Something went wrong. Please try again.";
      }
    } catch {
      dialogStatus.textContent = "Network error - check your connection.";
    }
  });
</script>

Replace https://send.webforms.to/wft_your_key with your own endpoint from the WebForms dashboard.

Why these fields

Where to send it

Get your endpoint

Free for 300 submissions a month. No card required.

Start free

Popup contact form FAQ

showModal() handles focus trapping, Escape and inertness of the page behind - the hard parts. You still owe it a visible close button and a sensible heading, both included here.