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.05compatibility: Claude Code, Codex, Gemini CLI6metadata:7 author: ai-skills8 version: "1.0"9 category: web-development10 tags: forms, validation, zod, react-hook-form, accessibility, error-handling11---1213## Overview1415Implements 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.1617## When to Use This Skill1819- Building or improving any form (login, registration, checkout, settings, multi-step wizards).20- The user mentions "form validation", "validate input", "error messages", or provides a form description.21- You need both client-side (UX) and server-side (security) validation.22- Ensuring WCAG-compliant error presentation.2324## Prerequisites2526- Form framework decided (plain HTML + JS, React, React Hook Form, Formik, etc.).27- Validation library: `zod` (strongly recommended), `yup`, or `joi`.28- For React: `react-hook-form` + `@hookform/resolvers` is the modern standard.29- Server-side endpoint that will receive the form data.30- Accessibility knowledge (or the skill will guide).3132## Steps33341. **Identify all fields and validation rules**:35 - Required/optional.36 - Type (text, email, number, date, file, etc.).37 - Format rules (min/max length, regex, range).38 - Cross-field validation (password confirmation, date start < end).39 - File-specific rules (type, size, count).40412. **Create a single source of truth schema** (Zod):42 - Define once, use for client (via resolver) and server (parse).43 - Use `.refine()` for cross-field rules.44 - Add `.transform()` for sanitization when safe.45463. **Decide validation timing strategy**:47 - **Real-time (onChange/onBlur)**: Best UX for long forms. Show errors as user types or leaves field.48 - **Submit-time only**: Simpler for short forms or when you want to avoid error noise.49 - Hybrid: Validate on blur + full validation on submit.50 - Document the chosen strategy and why.51524. **Implement accessible error display**:53 - Every invalid field gets `aria-invalid="true"`.54 - Error message gets a unique `id` and is referenced via `aria-describedby`.55 - Error messages are placed immediately after the input (or in a live region for complex cases).56 - Never rely on color alone; use icon + text.57 - For screen readers, use `role="alert"` on error containers when appropriate.58595. **Common field patterns** (include ready-to-use validators):60 - Email: `z.string().email()`61 - Phone: regex or `libphonenumber-js`62 - Password: length + complexity rules + confirmation match63 - Date ranges: custom refine64 - File upload: size, mime type, multiple files65 - URL, slug, username availability (async)66676. **React Hook Form + Zod integration** (most common):68 - `useForm({ resolver: zodResolver(schema) })`69 - `register` with proper props.70 - `FormProvider` for nested or complex forms.71 - `setError` for server-side errors after submit.72737. **Server-side validation**:74 - Always re-validate with the same schema.75 - Return field-specific errors in a consistent shape.76 - Never trust client-only validation.77788. **Output**:79 - Zod schema file or inline.80 - Client form component (or vanilla JS + HTML).81 - Server handler snippet.82 - Example of error response shape.83 - Accessibility notes and test instructions.8485## Examples8687**Example 1: Registration Form (React + React Hook Form + Zod)**8889```tsx90// schemas/registration.ts91import { z } from 'zod';9293export const registrationSchema = z.object({94 email: z.string().email('Please enter a valid email address'),95 password: z.string()96 .min(8, 'Password must be at least 8 characters')97 .regex(/[A-Z]/, 'Must contain at least one uppercase letter')98 .regex(/[0-9]/, 'Must contain at least one number'),99 confirmPassword: z.string(),100 fullName: z.string().min(2).max(100),101 acceptTerms: z.boolean().refine(val => val === true, {102 message: 'You must accept the terms and conditions',103 }),104}).refine((data) => data.password === data.confirmPassword, {105 message: "Passwords don't match",106 path: ['confirmPassword'],107});108109export type RegistrationInput = z.infer<typeof registrationSchema>;110```111112```tsx113// components/RegistrationForm.tsx114import { useForm } from 'react-hook-form';115import { zodResolver } from '@hookform/resolvers/zod';116import { registrationSchema, type RegistrationInput } from '@/schemas/registration';117118export function RegistrationForm() {119 const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<RegistrationInput>({120 resolver: zodResolver(registrationSchema),121 mode: 'onBlur',122 });123124 const onSubmit = async (data: RegistrationInput) => {125 // Server call here — server will also validate126 const res = await fetch('/api/register', { method: 'POST', body: JSON.stringify(data) });127 // handle response...128 };129130 return (131 <form onSubmit={handleSubmit(onSubmit)} noValidate>132 <div>133 <label htmlFor="email">Email</label>134 <input135 id="email"136 type="email"137 {...register('email')}138 aria-invalid={!!errors.email}139 aria-describedby={errors.email ? 'email-error' : undefined}140 className={errors.email ? 'border-red-500' : ''}141 />142 {errors.email && (143 <p id="email-error" role="alert" className="text-red-600 text-sm mt-1">144 {errors.email.message}145 </p>146 )}147 </div>148 {/* more fields... */}149 <button type="submit" disabled={isSubmitting}>Create account</button>150 </form>151 );152}153```154155**Server-side validation snippet** also provided.156157**Example 2: Vanilla HTML + JS form** with the same schema logic (using Zod in browser via CDN or bundled).158159## Edge Cases & Error Handling160161- **Async validation** (username availability): Use `useForm`'s `validate` or server-side only + debounced check. Show loading state on the field.162- **Server returns field errors after submit**: Use `setError(field, { type: 'server', message: 'Taken' })` for each field.163- **Very long forms / multi-step**: Validate only the current step on "Next". Keep one schema or split into step schemas.164- **File validation**: Check `file.size` and `file.type` client-side, but always re-validate on server (never trust client file metadata).165- **International phone numbers**: Recommend `libphonenumber-js` or `react-phone-number-input`.166- **Password managers**: Never disable autocomplete on login/registration fields.167- **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").168169## Verification1701711. Fill the form with valid data → submits successfully.1722. Submit with each invalid field one-by-one → correct, accessible error appears next to the field.1733. Test keyboard navigation and screen reader (VoiceOver/NVDA) — errors are announced when they appear.1744. Run axe DevTools on the form in error state — no violations related to forms or ARIA.1755. Submit the same invalid data directly to the server endpoint (bypass client) — server rejects with proper 422 and field errors.1766. Test password mismatch, file too large, etc.1777. Success: Users can correct errors without confusion. Screen reader users understand problems. Server is protected. No validation bypass possible.178179## References180181- [React Hook Form + Zod](https://react-hook-form.com/get-started#SchemaValidation)182- [Zod](https://zod.dev/)183- [WebAIM Form Validation](https://webaim.org/techniques/forms/validation)184- [ARIA Authoring Practices - Form](https://www.w3.org/WAI/ARIA/apg/patterns/form/)185- [OWASP Input Validation](https://cheatsheetseries.owasp.org/cheatsheets/Input_Validation_Cheat_Sheet.html)