Claude Fable 5.1 & GPT-6 Astra packages are live

Forms

Free · MIT

Forms that are accessible, resilient and correct — controlled state, validation timing, submission and error handling, and never trusting the client.

201 lines8.4 KB Kimi Frontend
Target models
Kimi K3Kimi K2.6Kimi K2 FamilyFuture Kimi Models
Name
forms
Category
Frontend
Description
Forms that are accessible, resilient and correct — controlled state, validation timing, submission and error handling, and never trusting the client.
License
MIT
Author
Agent.md maintainers
Last verified
2026-08-23
Reviewed by
unreviewed

#Scope contract

FILE_ISOLATION: Modify only files inside the scope the task names; report any out-of-scope change instead of making it.


#Purpose

Rules for building forms. Forms are where accessibility, validation, state management and security all meet, and where users lose work.

The first rule, which is not a frontend rule at all: client-side validation is a convenience, never a control. Every rule below assumes the server validates independently. → Backend/validation


#Use the platform

html
<form action="/orders" method="post">
  <label for="email">Email address</label>
  <input id="email" name="email" type="email" autocomplete="email" required
         aria-describedby="email-error" />
  <p id="email-error" role="alert">Enter a valid email address.</p>
  <button type="submit">Continue</button>
</form>

A real <form> with a submit button gives you Enter-to-submit, browser autofill, password-manager integration, and — with a progressively enhanced framework — a form that works before JavaScript loads.

AttributeEffect
type="email", tel, url, numberCorrect mobile keyboard and native validation
autocompleteAutofill; new-password and one-time-code matter especially
inputmode="numeric"Numeric keypad without number's spinner and scroll quirks
required, minlength, patternNative constraints, free
nameRequired for a form to submit without JavaScript
enterkeyhintLabels the mobile Enter key

Never use a <div onClick> as a submit control — it is not keyboard-operable and does not submit the form.


#Validate at the right moment

Validating on every keystroke tells the user their email is invalid after the first character. Validating only on submit hides errors until the end.

Field stateWhen to validate
Never touchedNever
Being typed, previously invalidOn change — show the fix immediately
Being typed, currently validOn blur
Submit attemptedAll fields, immediately

Define the schema once and use it on both sides:

ts
// shared/schemas.ts — one definition, both client and server
export const SignUp = z.object({
  email: z.string().email(),
  password: z.string().min(12).max(200),
}).strict();

Two schemas drift, and the drift always favours the client — the form accepts something the server rejects, and the user gets a generic 500.

Message rules: say what is wrong and how to fix it. "Invalid input" is not a message. Never blame the user, and never clear what they typed.


#Errors must be announced, not just coloured

html
<input aria-invalid="true" aria-describedby="pw-error" />
<p id="pw-error" role="alert">Password must be at least 12 characters.</p>
  • Associate the message with the input via aria-describedby, and set aria-invalid.
  • role="alert" on the error container so screen readers announce it.
  • Never convey an error by colour alone — colour-blind users see nothing. Combine colour with text and an icon.
  • On a failed submit, move focus to the first invalid field and, for a long form, show a summary at the top with links to each field.
  • Keep labels visible. Placeholder-as-label disappears on focus, fails contrast requirements, and breaks autofill. → Testing/accessibility

#Submission

tsx
async function onSubmit(values) {
  setStatus("submitting");                     // disable the button, show progress
  try {
    await api.createOrder(values, { idempotencyKey });   // stable across retries
    setStatus("success");
  } catch (err) {
    setStatus("error");
    setFieldErrors(err.fieldErrors ?? {});     // map server errors back to fields
  }
}
  • Disable the submit button while in flight, and use an idempotency key so a double-submit cannot create two orders. The button alone is not enough — the user can press Enter. → API/rest
  • Map server-side field errors back onto the fields. A form that shows "Validation failed" without saying which field is unusable.
  • Never clear the form on error. Losing typed data is the single most frustrating form bug.
  • Warn on navigating away with unsaved changes (beforeunload), and for long forms save a draft to localStorage — but never a draft containing a password or a card number.
  • Show a clear success state; a form that silently resets leaves users unsure whether it worked.

#Security

  • Never trust anything from the client: not hidden fields, not disabled attributes, not readonly values. All are editable in devtools.
  • Cross-site request forgery protection on every state-changing submission. → Security/csrf
  • File uploads: validate type by content server-side, cap size, and never trust the filename. → Backend/validation
  • Never log form values. A debug log of a signup form is a password disclosure.
  • autocomplete="off" on a password field fights password managers and makes users choose weaker passwords. Use new-password instead.

#Anti-patterns

Anti-patternWhy it failsFix
Client-side validation as the controlTrivially bypassedServer validates independently
Separate client and server schemasThey drift; the server rejects what the form acceptedOne shared schema
<div onClick> submitNot keyboard-operable; no native submit<button type="submit">
No name attributesCannot submit without JavaScriptName every field
Validating on every keystrokeErrors before the user finishes typingValidate on blur, then on change
Errors shown only by colourInvisible to colour-blind usersText plus icon
Errors not associated with inputsScreen readers never announce themaria-describedby + role="alert"
Placeholder as labelDisappears; fails contrast; breaks autofillVisible <label>
No submit-in-flight stateDouble submission creates duplicatesDisable plus idempotency key
Server errors not mapped to fieldsUser cannot tell what to fixField-level error mapping
Clearing the form on errorUsers lose their workPreserve input
No unsaved-changes warningAccidental navigation loses everythingbeforeunload and drafts
Drafts containing secretsPasswords persisted in localStorageExclude sensitive fields
Trusting hidden or disabled fieldsEditable in devtoolsRe-derive server-side
Missing autocompleteBreaks autofill and password managersCorrect token per field
autocomplete="off" on passwordsEncourages weaker passwordsnew-password
Logging form payloadsPassword disclosureNever log values

#Checklist

  • Verify: Every form is a real <form> with a submit button and named fields
  • Verify: Appropriate type, inputmode and autocomplete are set per field
  • Verify: Native constraint attributes are used where they apply
  • Verify: One schema definition validates on both client and server
  • Verify: Validation runs on blur first, then on change once a field is invalid
  • Verify: Error messages state the problem and the fix
  • Verify: Errors are associated with aria-describedby and announced with role="alert"
  • Verify: aria-invalid is set on failing fields
  • Verify: Errors are never conveyed by colour alone
  • Verify: Labels are visible and associated; placeholders are not used as labels
  • Verify: Focus moves to the first invalid field on failed submit
  • Verify: The submit control is disabled while in flight
  • Verify: Submissions carry an idempotency key
  • Verify: Server field errors are mapped back onto the fields
  • Verify: Input is never cleared on error
  • Verify: Unsaved-change warnings and drafts exist for long forms, excluding secrets
  • Verify: CSRF protection covers every state-changing submission
  • Verify: No form values are logged