A static site still needs a contact form, and the honest options are fewer and better than any stackOverflow thread implies. The pattern is the same for all of them: browser validation stays the validator, and the destination changes only.

const form = document.querySelector("#contact");
if (!form.reportValidity()) return; // browser already showed the errors

const data = Object.fromEntries(new FormData(form));
const body = JSON.stringify(data, null, 2);

// Destination A: the mail object, zero backend, opens the reader
location.href =
  "mailto:hello@example.com" +
  "?subject=" + encodeURIComponent(data.subject) +
  "&body=" + encodeURIComponent(body);

The destination ladder, easiest last

  • GET method to a search or results page: the most forgiving static contact form, and it is this site's own search behavior.
  • mailto with subject and body prebuilt: no server lines anywhere, no privacy tradeoff beyond the mail client itself.
  • Your social or CRM's public form endpoints: the third-party takes the data, so read that privacy page twice before wiring it up.
  • Host-provided form services: some static hosts ship a forms feature (Netlify's is one); it is a real backend installed under your URL, and its spam story comes with your host.

The validation layer

  1. required, type="email", minlength: browser constraints first, error rendering for free.
  2. reportValidity() for programmatic paths: one call surfaces the browser's native error bubble where the form left off.
  3. A custom visual layer on top when the design needs it: this site's form-validity article has the CSS recipe.
  4. Honeypot fields on backend cases and rate limits from the host; static does not mean unspammable, say that loudly.

The gacor summary: the browser validates, the platform transports, and your build step ships nothing. Contact form libraries are a symptom of forgetting that the form element never left.