enhance-web-forms — Production-Quality Forms
Forms are where users hand you their data and where apps most often feel broken: unlabeled
fields, validation that fires on every keystroke, errors screen readers never announce, a
submit button that does nothing visible for three seconds, and no recovery when the request
fails. This skill fixes all of that on real forms in the repo.
A form is done when it is accessible, validated on both sides, gives feedback for every
state, and recovers from failure. Compile-clean is not done.
Before any browser interaction, read protocol-browser-anti-stall and apply it.
Phase 0 — Detect the stack and inventory forms
cat package.json | grep -iE "react-hook-form|formik|@tanstack/react-form|final-form|zod|yup|valibot|superstruct"
rg -n "<form|onSubmit|useForm|handleSubmit" -g "*.{tsx,jsx,vue,svelte}" -l
Record: form library (or native), validation/schema library, UI/component library, and the
list of forms to enhance (auth, checkout, settings, contact, search, etc.). If there are no
forms, bow out.
Phase 1 — Research + prioritize
Follow /research: Context7 for the detected form/validation library's current API;
Firecrawl for current form UX/a11y guidance dated to now. Prioritize forms by traffic and
risk (auth, payment, destructive actions first).
Phase 2 — Accessible structure (the foundation)
For each form, verify/fix:
- Every input has a programmatic label —
<label htmlFor> or aria-label; placeholder
is not a label.
- Correct input types +
autocomplete — type="email|tel|url|number", inputmode,
autocomplete="email|current-password|cc-number|..." for autofill.
- Grouping — related inputs in
<fieldset> + <legend> (radio groups, address blocks).
- Required + optional — marked in text, not color alone;
required / aria-required.
- Keyboard + focus — logical tab order, visible focus ring, Enter submits, no keyboard traps.
- Error association — each field error linked via
aria-describedby; invalid fields get
aria-invalid="true"; a form-level error summary with links to fields on submit failure.
rg -n "placeholder=" -g "*.{tsx,jsx}" # placeholders masquerading as labels?
rg -n "aria-describedby|aria-invalid|htmlFor|aria-label" -g "*.{tsx,jsx}" -c
Phase 3 — Validation (schema-driven, both sides)
- Single schema as SSOT — define validation once (zod/yup/valibot) and share it
between client and server so rules can't drift. If the backend validates separately,
reconcile the two so error shapes match.
- Timing — validate on blur and on submit, not on every keystroke; re-validate a
field on change after it has errored once (so users see fixes immediately).
- Messages — specific and actionable ("Password needs 8+ characters", not "Invalid").
- Server errors — map field-level server errors back onto the right inputs; show
form-level errors (e.g. "Email already registered") in the summary.
Phase 4 — States & feedback (every one, no gaps)
Wire the full state machine for each form and submit:
| State |
Required behavior |
| Idle |
Clean, submit enabled/disabled per validity policy |
| Validating |
Inline field feedback (see Phase 3 timing) |
| Submitting |
Submit shows spinner/label swap + disabled + aria-busy; prevent double-submit |
| Success |
Confirmation (toast/inline/redirect); reset or lock as appropriate |
| Error |
Preserve entered values; surface the failure; keep the user's place; retry path |
| Empty/optional |
Sensible defaults; empty state for dependent selects |
Also handle:
- Multi-step — progress indicator, per-step validation, back/forward preserves data,
final review before submit.
- Unsaved-changes guard — warn on navigation away from a dirty form.
- Autosave / draft — for long forms, if the app already has a persistence pattern.
- Micro-feedback — subtle transitions on error/success via
design-motion conventions
(reduced-motion safe). Don't animate layout in a way that shifts fields.
Phase 5 — Verify and report
- playwright-cli: walk each form — tab through it, submit empty (see the error summary +
focus moves to first error), submit invalid, submit valid, force a server error. Check
console. Confirm screen-reader names via the accessibility snapshot.
Screenshots to .playwright-mcp/.
- Build/typecheck/lint: run the repo's commands.
## Forms Enhancement — report
**Stack:** form=[..] · validation=[..] · UI=[..]
**Forms upgraded:** [list]
**Structure:** labels/types/autocomplete/fieldsets fixed on [N] forms
**Validation:** shared schema SSOT · client↔server parity · blur+submit timing
**States:** submitting/success/error/multi-step/unsaved-guard wired
**A11y:** error summary + aria-describedby/aria-invalid + focus management (evidence)
**Verification:** build ✓ · console clean ✓ · flows walked (screenshots [paths])
Related
audit-accessibility — deep WCAG audit (run to verify the a11y work)
audit-ux — form usability heuristics, microcopy, cognitive load
audit-fe-api — validate the submit request/response contract against the backend
backend-error-handling — server-side validation + structured error responses
design-motion — reduced-motion-safe micro-feedback for error/success
enhance-web-ux — broader page/flow UX beyond the form itself
1---2name: enhance-web-forms3description: Build or upgrade web forms to production quality: accessible structure, schema-driven validation, client↔server parity. Use when "improve this form", "form validation", "accessible form", "multi-step form", "form error handling", or "the form UX is bad".4license: MIT5---67# enhance-web-forms — Production-Quality Forms89Forms are where users hand you their data and where apps most often feel broken: unlabeled10fields, validation that fires on every keystroke, errors screen readers never announce, a11submit button that does nothing visible for three seconds, and no recovery when the request12fails. This skill fixes all of that on real forms in the repo.1314> **A form is done when it is accessible, validated on both sides, gives feedback for every15> state, and recovers from failure.** Compile-clean is not done.1617**Before any browser interaction, read `protocol-browser-anti-stall` and apply it.**1819---2021## Phase 0 — Detect the stack and inventory forms2223```bash24cat package.json | grep -iE "react-hook-form|formik|@tanstack/react-form|final-form|zod|yup|valibot|superstruct"25rg -n "<form|onSubmit|useForm|handleSubmit" -g "*.{tsx,jsx,vue,svelte}" -l26```2728Record: form library (or native), validation/schema library, UI/component library, and the29list of forms to enhance (auth, checkout, settings, contact, search, etc.). If there are no30forms, bow out.3132---3334## Phase 1 — Research + prioritize3536Follow `/research`: Context7 for the detected form/validation library's current API;37Firecrawl for current form UX/a11y guidance dated to now. Prioritize forms by traffic and38risk (auth, payment, destructive actions first).3940---4142## Phase 2 — Accessible structure (the foundation)4344For each form, verify/fix:4546- **Every input has a programmatic label** — `<label htmlFor>` or `aria-label`; placeholder47 is **not** a label.48- **Correct input types + `autocomplete`** — `type="email|tel|url|number"`, `inputmode`,49 `autocomplete="email|current-password|cc-number|..."` for autofill.50- **Grouping** — related inputs in `<fieldset>` + `<legend>` (radio groups, address blocks).51- **Required + optional** — marked in text, not color alone; `required` / `aria-required`.52- **Keyboard + focus** — logical tab order, visible focus ring, Enter submits, no keyboard traps.53- **Error association** — each field error linked via `aria-describedby`; invalid fields get54 `aria-invalid="true"`; a form-level error **summary** with links to fields on submit failure.5556```bash57rg -n "placeholder=" -g "*.{tsx,jsx}" # placeholders masquerading as labels?58rg -n "aria-describedby|aria-invalid|htmlFor|aria-label" -g "*.{tsx,jsx}" -c59```6061---6263## Phase 3 — Validation (schema-driven, both sides)6465- **Single schema as SSOT** — define validation once (zod/yup/valibot) and share it66 between client and server so rules can't drift. If the backend validates separately,67 reconcile the two so error shapes match.68- **Timing** — validate on **blur** and on **submit**, not on every keystroke; re-validate a69 field on change **after** it has errored once (so users see fixes immediately).70- **Messages** — specific and actionable ("Password needs 8+ characters", not "Invalid").71- **Server errors** — map field-level server errors back onto the right inputs; show72 form-level errors (e.g. "Email already registered") in the summary.7374---7576## Phase 4 — States & feedback (every one, no gaps)7778Wire the full state machine for each form and submit:7980| State | Required behavior |81|---|---|82| Idle | Clean, submit enabled/disabled per validity policy |83| Validating | Inline field feedback (see Phase 3 timing) |84| Submitting | Submit shows spinner/label swap + `disabled` + `aria-busy`; prevent double-submit |85| Success | Confirmation (toast/inline/redirect); reset or lock as appropriate |86| Error | Preserve entered values; surface the failure; keep the user's place; retry path |87| Empty/optional | Sensible defaults; empty state for dependent selects |8889Also handle:90- **Multi-step** — progress indicator, per-step validation, back/forward preserves data,91 final review before submit.92- **Unsaved-changes guard** — warn on navigation away from a dirty form.93- **Autosave / draft** — for long forms, if the app already has a persistence pattern.94- **Micro-feedback** — subtle transitions on error/success via `design-motion` conventions95 (reduced-motion safe). Don't animate layout in a way that shifts fields.9697---9899## Phase 5 — Verify and report100101- **playwright-cli:** walk each form — tab through it, submit empty (see the error summary +102 focus moves to first error), submit invalid, submit valid, force a server error. Check103 `console`. Confirm screen-reader names via the accessibility snapshot.104 Screenshots to `.playwright-mcp/`.105- **Build/typecheck/lint:** run the repo's commands.106107```markdown108## Forms Enhancement — report109**Stack:** form=[..] · validation=[..] · UI=[..]110**Forms upgraded:** [list]111**Structure:** labels/types/autocomplete/fieldsets fixed on [N] forms112**Validation:** shared schema SSOT · client↔server parity · blur+submit timing113**States:** submitting/success/error/multi-step/unsaved-guard wired114**A11y:** error summary + aria-describedby/aria-invalid + focus management (evidence)115**Verification:** build ✓ · console clean ✓ · flows walked (screenshots [paths])116```117118---119120## Related121122- `audit-accessibility` — deep WCAG audit (run to verify the a11y work)123- `audit-ux` — form usability heuristics, microcopy, cognitive load124- `audit-fe-api` — validate the submit request/response contract against the backend125- `backend-error-handling` — server-side validation + structured error responses126- `design-motion` — reduced-motion-safe micro-feedback for error/success127- `enhance-web-ux` — broader page/flow UX beyond the form itself