send.webforms.toRead the docs →

Inline validation that doesn't fight the browser

Most validation snippets fight the browser: they suppress native validation, then rebuild half of it badly, usually ending in an alert box. This version layers on top instead - native constraints still run, but errors render as inline text under each field at the right moment (on blur, not on every keystroke), with the aria wiring screen readers need.

Copy-paste HTML

index.html
<form id="validated-form" action="https://send.webforms.to/wft_your_key" method="POST" novalidate>
  <div>
    <label for="v-name">Name</label>
    <input id="v-name" name="name" type="text" required minlength="2">
    <p class="field-error" aria-live="polite"></p>
  </div>

  <div>
    <label for="v-email">Email</label>
    <input id="v-email" name="email" type="email" required>
    <p class="field-error" aria-live="polite"></p>
  </div>

  <div>
    <label for="v-message">Message</label>
    <textarea id="v-message" name="message" required minlength="10"></textarea>
    <p class="field-error" aria-live="polite"></p>
  </div>

  <button type="submit">Send</button>
</form>

<style>
  .field-error { color: #c0392b; font-size: 0.85em; min-height: 1.2em; margin: 0.25em 0 0; }
  [aria-invalid="true"] { border-color: #c0392b; }
</style>

<script>
  const form = document.getElementById("validated-form");

  function messageFor(input) {
    const v = input.validity;
    if (v.valueMissing) return "This field is required.";
    if (v.typeMismatch) return "That doesn't look like a valid email.";
    if (v.tooShort)
      return `Needs at least ${input.minLength} characters (currently ${input.value.length}).`;
    return "";
  }

  function showError(input) {
    const msg = messageFor(input);
    input.setAttribute("aria-invalid", msg ? "true" : "false");
    input.closest("div").querySelector(".field-error").textContent = msg;
    return !msg;
  }

  for (const input of form.querySelectorAll("input, textarea")) {
    input.addEventListener("blur", () => showError(input));
    input.addEventListener("input", () => {
      if (input.getAttribute("aria-invalid") === "true") showError(input);
    });
  }

  form.addEventListener("submit", (e) => {
    let ok = true;
    for (const input of form.querySelectorAll("input, textarea")) {
      if (!showError(input)) ok = false;
    }
    if (!ok) {
      e.preventDefault();
      form.querySelector('[aria-invalid="true"]').focus();
    }
  });
</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

Form with JavaScript validation FAQ

No. It's UX, not security - anyone can POST past it with curl. The endpoint still validates and spam-filters everything on arrival.