Validate forms on both sides: HTML5 constraint attributes, Constraint Validation API for inline accessible errors, submit guards against double-submits, server-side re-validation, and security gotchas. Triggers on: form validation, validate a form, inline form errors, constraint validation, server-side validation, form error UX, accessible form errors, duplicate submit.
./install.sh form-validationname: "form_validation" description: "Validate forms on both sides: HTML5 constraint attributes, Constraint Validation API for inline accessible errors, submit guards against double-submits, server-side re-validation, and security gotchas. Triggers on: form validation, validate a form, inline form errors, constraint validation, server-side validation, form error UX, accessible form errors, duplicate submit." metadata: { "includeInPrompt": true }
Practical validation for forms an agent builds or consumes: declarative client-side checks with accessible error UX, submit guards that stop double-submits, and a server side that re-validates everything from scratch — plus the security gotchas that live specifically in the form layer. This skill is the form layer only: authentication flows belong to api-auth-patterns, and durable duplicate handling via idempotency keys is covered by webhook-handling (cross-referenced below).
html
<form id="signup" method="post" action="/signup" novalidate>
<label for="email">Email</label>
<input id="email" name="email" type="email" required maxlength="254"
aria-describedby="email-error">
<p id="email-error" class="error" aria-live="polite"></p>
<label for="handle">Handle</label>
<input id="handle" name="handle" type="text" required
minlength="3" maxlength="24" pattern="[A-Za-z0-9_]+"
aria-describedby="handle-error">
<p id="handle-error" class="error" aria-live="polite"></p>
<label for="age">Age</label>
<input id="age" name="age" type="number" min="13" max="120" required
aria-describedby="age-error">
<p id="age-error" class="error" aria-live="polite"></p>
</form>
Rules of thumb: type="email" over hand-rolled email regexes; maxlength on every text input (it is also a cheap injection/DOS bound); pattern for simple formats only (handles, slugs, postal codes); min/max/step for numbers; required where the server also requires it. Keep attribute rules and server rules identical — drift between them is where bugs live. (novalidate is there because step 2 replaces native bubbles with inline errors; drop it if you want the browser's default UI instead.)
setCustomValidity(), checkValidity(), reportValidity(), validationMessage, and the validity object. Pattern: validate on blur (per field) and on submit (whole form); clear custom errors before re-checking; point each field at its error container with aria-describedby and flip aria-invalid:js
const form = document.getElementById("signup");
function validateField(field) {
const err = document.getElementById(field.getAttribute("aria-describedby"));
field.setCustomValidity(""); // clear any previous custom error first
if (field.id === "handle" && field.value.includes("admin")) {
field.setCustomValidity("That handle is reserved.");
}
const ok = field.checkValidity();
field.setAttribute("aria-invalid", String(!ok));
if (err) err.textContent = ok ? "" : field.validationMessage;
return ok;
}
form.addEventListener("focusout", (e) => {
if (e.target.willValidate) validateField(e.target);
});
form.addEventListener("submit", (e) => {
let firstBad = null;
for (const field of form.elements) {
if (field.willValidate && !validateField(field) && !firstBad) firstBad = field;
}
if (firstBad) { e.preventDefault(); firstBad.focus(); }
});
Why this shape: aria-live="polite" on the error container makes screen readers announce the message; moving focus to the first invalid field on submit keeps keyboard users oriented; validating on blur avoids punishing users while they type. If you prefer native bubbles instead, drop novalidate and call form.reportValidity() on submit — but never rely on native UI alone for anything complex.
webhook-handling (unique key constraint + idempotent handler).undefined slip through)."abc" as age must fail, not become 0).minlength/maxlength as the client — a missing server-side max is a storage/DOS vector.pattern; for emails, a simple local@domain shape check plus a confirmation/send-to-verify step beats a perfect regex.<select> or radio group. Re-check membership in the allowed set (plan in {"free","pro","team"}) — option lists are trivially forged."admin "-style edge cases through.Return errors keyed by field name ({"handle": "That handle is reserved."}) so the client can map them back to the same inline error containers from step 2.
"User <script>…" not found is reflected XSS. Escape or drop the value.https only), reject credentials in the URL (user:pass@host), localhost/private IPs, and redirects — treat it as an outbound-request problem, not just a format check.429 responses with backoff instead of retrying blindly.After applying this skill, an agent should be able to produce:
required, type, minlength/maxlength, pattern, min/max) and whose errors render inline: aria-describedby-linked containers, aria-live announcements, aria-invalid toggling, blur + submit validation via the Constraint Validation API, focus moved to the first invalid field.webhook-handling), not just a disabled button.429.publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e
No ratings yet — be the first to rate it.