Overview
Implements robust, accessible form validation that works on both client and server. The skill produces validation schemas (Zod recommended), real-time and submit-time validation strategies, accessible error display patterns (aria-invalid, aria-describedby), common field validators (email, phone, password strength, date ranges, file uploads), and full integration examples with React Hook Form + Zod or vanilla HTML forms.
When to Use This Skill
- Building or improving any form (login, registration, checkout, settings, multi-step wizards).
- The user mentions "form validation", "validate input", "error messages", or provides a form description.
- You need both client-side (UX) and server-side (security) validation.
- Ensuring WCAG-compliant error presentation.
Prerequisites
- Form framework decided (plain HTML + JS, React, React Hook Form, Formik, etc.).
- Validation library:
zod (strongly recommended), yup, or joi.
- For React:
react-hook-form + @hookform/resolvers is the modern standard.
- Server-side endpoint that will receive the form data.
- Accessibility knowledge (or the skill will guide).
Steps
Identify all fields and validation rules:
- Required/optional.
- Type (text, email, number, date, file, etc.).
- Format rules (min/max length, regex, range).
- Cross-field validation (password confirmation, date start < end).
- File-specific rules (type, size, count).
Create a single source of truth schema (Zod):
- Define once, use for client (via resolver) and server (parse).
- Use
.refine() for cross-field rules.
- Add
.transform() for sanitization when safe.
Decide validation timing strategy:
- Real-time (onChange/onBlur): Best UX for long forms. Show errors as user types or leaves field.
- Submit-time only: Simpler for short forms or when you want to avoid error noise.
- Hybrid: Validate on blur + full validation on submit.
- Document the chosen strategy and why.
Implement accessible error display:
- Every invalid field gets
aria-invalid="true".
- Error message gets a unique
id and is referenced via aria-describedby.
- Error messages are placed immediately after the input (or in a live region for complex cases).
- Never rely on color alone; use icon + text.
- For screen readers, use
role="alert" on error containers when appropriate.
Common field patterns (include ready-to-use validators):
- Email:
z.string().email()
- Phone: regex or
libphonenumber-js
- Password: length + complexity rules + confirmation match
- Date ranges: custom refine
- File upload: size, mime type, multiple files
- URL, slug, username availability (async)
React Hook Form + Zod integration (most common):
useForm({ resolver: zodResolver(schema) })
register with proper props.
FormProvider for nested or complex forms.
setError for server-side errors after submit.
Server-side validation:
- Always re-validate with the same schema.
- Return field-specific errors in a consistent shape.
- Never trust client-only validation.
Output:
- Zod schema file or inline.
- Client form component (or vanilla JS + HTML).
- Server handler snippet.
- Example of error response shape.
- Accessibility notes and test instructions.
Examples
Example 1: Registration Form (React + React Hook Form + Zod)
// schemas/registration.ts
import { z } from 'zod';
export const registrationSchema = z.object({
email: z.string().email('Please enter a valid email address'),
password: z.string()
.min(8, 'Password must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain at least one uppercase letter')
.regex(/[0-9]/, 'Must contain at least one number'),
confirmPassword: z.string(),
fullName: z.string().min(2).max(100),
acceptTerms: z.boolean().refine(val => val === true, {
message: 'You must accept the terms and conditions',
}),
}).refine((data) => data.password === data.confirmPassword, {
message: "Passwords don't match",
path: ['confirmPassword'],
});
export type RegistrationInput = z.infer<typeof registrationSchema>;
// components/RegistrationForm.tsx
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { registrationSchema, type RegistrationInput } from '@/schemas/registration';
export function RegistrationForm() {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<RegistrationInput>({
resolver: zodResolver(registrationSchema),
mode: 'onBlur',
});
const (data: RegistrationInput) => {
// Server call here — server will also validate
const res = await fetch('/api/register', { method: 'POST', body: JSON.stringify(data) });
// handle response...
};
return (
<form noValidate>
<div>
<label htmlFor="email">Email</label>
<input
id="email"
type="email"
{...register('email')}
aria-invalid={!!errors.email}
aria-describedby={errors.email ? 'email-error' : undefined}
className={errors.email ? 'border-red-500' : ''}
/>
{errors.email && (
<p id="email-error" role="alert" className="text-red-600 text-sm mt-1">
{errors.email.message}
</p>
)}
</div>
{/* more fields... */}
<button type="submit" disabled={isSubmitting}>Create account</button>
</form>
);
}
Server-side validation snippet also provided.
Example 2: Vanilla HTML + JS form with the same schema logic (using Zod in browser via CDN or bundled).
Edge Cases & Error Handling
- Async validation (username availability): Use
useForm's validate or server-side only + debounced check. Show loading state on the field.
- Server returns field errors after submit: Use
setError(field, { type: 'server', message: 'Taken' }) for each field.
- Very long forms / multi-step: Validate only the current step on "Next". Keep one schema or split into step schemas.
- File validation: Check
file.size and file.type client-side, but always re-validate on server (never trust client file metadata).
- International phone numbers: Recommend
libphonenumber-js or react-phone-number-input.
- Password managers: Never disable autocomplete on login/registration fields.
- Error message clarity: Write messages that tell the user what to do, not just what is wrong ("Enter a valid email" vs "Invalid email format").
Verification
- Fill the form with valid data → submits successfully.
- Submit with each invalid field one-by-one → correct, accessible error appears next to the field.
- Test keyboard navigation and screen reader (VoiceOver/NVDA) — errors are announced when they appear.
- Run axe DevTools on the form in error state — no violations related to forms or ARIA.
- Submit the same invalid data directly to the server endpoint (bypass client) — server rejects with proper 422 and field errors.
- Test password mismatch, file too large, etc.
- Success: Users can correct errors without confusion. Screen reader users understand problems. Server is protected. No validation bypass possible.
References
1---2name: form-validator3description: Implements client-side and server-side form validation with accessible error messaging. Use when adding validation to any HTML or React form.4license: Apache-2.05---67## Overview89Implements robust, accessible form validation that works on both client and server. The skill produces validation schemas (Zod recommended), real-time and submit-time validation strategies, accessible error display patterns (`aria-invalid`, `aria-describedby`), common field validators (email, phone, password strength, date ranges, file uploads), and full integration examples with React Hook Form + Zod or vanilla HTML forms.1011## When to Use This Skill1213- Building or improving any form (login, registration, checkout, settings, multi-step wizards).14- The user mentions "form validation", "validate input", "error messages", or provides a form description.15- You need both client-side (UX) and server-side (security) validation.16- Ensuring WCAG-compliant error presentation.1718## Prerequisites1920- Form framework decided (plain HTML + JS, React, React Hook Form, Formik, etc.).21- Validation library: `zod` (strongly recommended), `yup`, or `joi`.22- For React: `react-hook-form` + `@hookform/resolvers` is the modern standard.23- Server-side endpoint that will receive the form data.24- Accessibility knowledge (or the skill will guide).2526## Steps27281. **Identify all fields and validation rules**:29 - Required/optional.30 - Type (text, email, number, date, file, etc.).31 - Format rules (min/max length, regex, range).32 - Cross-field validation (password confirmation, date start < end).33 - File-specific rules (type, size, count).34352. **Create a single source of truth schema** (Zod):36 - Define once, use for client (via resolver) and server (parse).37 - Use `.refine()` for cross-field rules.38 - Add `.transform()` for sanitization when safe.39403. **Decide validation timing strategy**:41 - **Real-time (onChange/onBlur)**: Best UX for long forms. Show errors as user types or leaves field.42 - **Submit-time only**: Simpler for short forms or when you want to avoid error noise.43 - Hybrid: Validate on blur + full validation on submit.44 - Document the chosen strategy and why.45464. **Implement accessible error display**:47 - Every invalid field gets `aria-invalid="true"`.48 - Error message gets a unique `id` and is referenced via `aria-describedby`.49 - Error messages are placed immediately after the input (or in a live region for complex cases).50 - Never rely on color alone; use icon + text.51 - For screen readers, use `role="alert"` on error containers when appropriate.52535. **Common field patterns** (include ready-to-use validators):54 - Email: `z.string().email()`55 - Phone: regex or `libphonenumber-js`56 - Password: length + complexity rules + confirmation match57 - Date ranges: custom refine58 - File upload: size, mime type, multiple files59 - URL, slug, username availability (async)60616. **React Hook Form + Zod integration** (most common):62 - `useForm({ resolver: zodResolver(schema) })`63 - `register` with proper props.64 - `FormProvider` for nested or complex forms.65 - `setError` for server-side errors after submit.66677. **Server-side validation**:68 - Always re-validate with the same schema.69 - Return field-specific errors in a consistent shape.70 - Never trust client-only validation.71728. **Output**:73 - Zod schema file or inline.74 - Client form component (or vanilla JS + HTML).75 - Server handler snippet.76 - Example of error response shape.77 - Accessibility notes and test instructions.7879## Examples8081**Example 1: Registration Form (React + React Hook Form + Zod)**8283```tsx84// schemas/registration.ts85import { z } from 'zod';8687export const registrationSchema = z.object({88 email: z.string().email('Please enter a valid email address'),89 password: z.string()90 .min(8, 'Password must be at least 8 characters')91 .regex(/[A-Z]/, 'Must contain at least one uppercase letter')92 .regex(/[0-9]/, 'Must contain at least one number'),93 confirmPassword: z.string(),94 fullName: z.string().min(2).max(100),95 acceptTerms: z.boolean().refine(val => val === true, {96 message: 'You must accept the terms and conditions',97 }),98}).refine((data) => data.password === data.confirmPassword, {99 message: "Passwords don't match",100 path: ['confirmPassword'],101});102103export type RegistrationInput = z.infer<typeof registrationSchema>;104```105106```tsx107// components/RegistrationForm.tsx108import { useForm } from 'react-hook-form';109import { zodResolver } from '@hookform/resolvers/zod';110import { registrationSchema, type RegistrationInput } from '@/schemas/registration';111112export function RegistrationForm() {113 const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<RegistrationInput>({114 resolver: zodResolver(registrationSchema),115 mode: 'onBlur',116 });117118 const onSubmit = async (data: RegistrationInput) => {119 // Server call here — server will also validate120 const res = await fetch('/api/register', { method: 'POST', body: JSON.stringify(data) });121 // handle response...122 };123124 return (125 <form onSubmit={handleSubmit(onSubmit)} noValidate>126 <div>127 <label htmlFor="email">Email</label>128 <input129 id="email"130 type="email"131 {...register('email')}132 aria-invalid={!!errors.email}133 aria-describedby={errors.email ? 'email-error' : undefined}134 className={errors.email ? 'border-red-500' : ''}135 />136 {errors.email && (137 <p id="email-error" role="alert" className="text-red-600 text-sm mt-1">138 {errors.email.message}139 </p>140 )}141 </div>142 {/* more fields... */}143 <button type="submit" disabled={isSubmitting}>Create account</button>144 </form>145 );146}147```148149**Server-side validation snippet** also provided.150151**Example 2: Vanilla HTML + JS form** with the same schema logic (using Zod in browser via CDN or bundled).152153## Edge Cases & Error Handling154155- **Async validation** (username availability): Use `useForm`'s `validate` or server-side only + debounced check. Show loading state on the field.156- **Server returns field errors after submit**: Use `setError(field, { type: 'server', message: 'Taken' })` for each field.157- **Very long forms / multi-step**: Validate only the current step on "Next". Keep one schema or split into step schemas.158- **File validation**: Check `file.size` and `file.type` client-side, but always re-validate on server (never trust client file metadata).159- **International phone numbers**: Recommend `libphonenumber-js` or `react-phone-number-input`.160- **Password managers**: Never disable autocomplete on login/registration fields.161- **Error message clarity**: Write messages that tell the user what to do, not just what is wrong ("Enter a valid email" vs "Invalid email format").162163## Verification1641651. Fill the form with valid data → submits successfully.1662. Submit with each invalid field one-by-one → correct, accessible error appears next to the field.1673. Test keyboard navigation and screen reader (VoiceOver/NVDA) — errors are announced when they appear.1684. Run axe DevTools on the form in error state — no violations related to forms or ARIA.1695. Submit the same invalid data directly to the server endpoint (bypass client) — server rejects with proper 422 and field errors.1706. Test password mismatch, file too large, etc.1717. Success: Users can correct errors without confusion. Screen reader users understand problems. Server is protected. No validation bypass possible.172173## References174175- [React Hook Form + Zod](https://react-hook-form.com/get-started#SchemaValidation)176- [Zod](https://zod.dev/)177- [WebAIM Form Validation](https://webaim.org/techniques/forms/validation)178- [ARIA Authoring Practices - Form](https://www.w3.org/WAI/ARIA/apg/patterns/form/)179- [OWASP Input Validation](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html)