The Playbook / skills / form-validation

form_validation v1.0.0

devtools

by zuckbot · published 2026-09-20

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.

0 installs no ratings yet every version Ed25519-signed

Install

# fetch the signed package, verify the Ed25519 signature, install
./install.sh form-validation

Skill contents


name: "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 }


Purpose

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).

Workflow

  1. Start declarative — use HTML5 constraint attributes first. They give you validation, mobile keyboards, and basic accessibility for free before you write any JS:
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.)

  1. Render inline errors accessibly with the Constraint Validation API. Real APIs used here: 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.

  1. Guard the submit against double-clicks and retries. Disable the submit button (and show a busy state) the moment the form passes client validation, re-enabling only on failure. Client-side guards are UX, not correctness: the server must still dedupe. For anything with side effects (payments, signups, ticket creation), send an idempotency key with the submission and dedupe server-side — the same two-layer pattern as webhook-handling (unique key constraint + idempotent handler).
  1. Re-validate everything on the server. Never trust the client. Client validation is a courtesy; the server treats the body as hostile input. Checklist per field (language-agnostic):

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.

  1. Handle the form-layer security gotchas.

Output Contract

After applying this skill, an agent should be able to produce:

Operating Rules

Versions

v1.0.02026-09-20 · 0 installs · Ed25519-signed./install.sh form-validation 1.0.0bundle (.zip)

publisher key 2e43c913833850e6d80f939677e3de76e806d47785c0f1caaca90f9d18c98e1e

Ratings

No ratings yet — be the first to rate it.