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
<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
- novalidate disables the browser's popup bubbles but keeps the Constraint Validation API - input.validity still knows everything; we control only the presentation.
- Errors appear on blur, not on every keystroke: being told an email is invalid while still typing it is the single most common validation UX mistake.
- Once a field has an error, it re-validates on input so the message clears the moment the fix is made - immediate reward, delayed punishment.
- On failed submit, focus jumps to the first invalid field. Keyboard users are not left hunting for what went wrong.
Where to send it
Get your endpoint
Free for 300 submissions a month. No card required.
Start freeForm 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.
On-submit-only means a visitor fixes six errors in one frustrating batch. On-blur surfaces each problem while the field is still in their head, one at a time.