Form Validation Library
Battle-tested form patterns built on React Hook Form and Zod that handle the hard parts of form development: multi-step wizards with state persistence, async field validation, file upload with drag-and-drop, dependent field logic, accessible error announcements, and type-safe form schemas that double as API request validators. Includes 20+ ready-to-use form templates covering authentication, checkout, profile editing, surveys, and admin CRUD — each with full TypeScript types and zero runtime dependencies beyond React Hook Form and Zod.
Key Features
- Zod Schema Validation — Type-safe schemas that validate on the client and reuse on the server; define once, validate everywhere
- Multi-Step Form Wizard — Step navigation with validation-per-step, progress persistence to sessionStorage, and back/forward support
-
Accessible Error Handling — Errors announced to screen readers via
aria-live, focus management on submission failure, and inline validation - File Upload Patterns — Drag-and-drop zone with preview, file type/size validation, multi-file uploads, and upload progress tracking
- Dependent Fields — Watch-based conditional rendering where field B's options change based on field A's value, with proper cleanup
- Async Validation — Debounced server-side validation (e.g., "is this username taken?") with loading indicators
- Form State Persistence — Auto-save drafts to localStorage with configurable debounce, restore on page reload
- Server Action Integration — Patterns for Next.js Server Actions with progressive enhancement and optimistic UI
Quick Start
- Install dependencies:
npm install react-hook-form zod @hookform/resolvers
Copy the
forms/directory into your project'ssrc/lib/folder.Create a validated form in under 30 lines:
import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';
import { FormField, FormError } from '@/lib/forms/components';
const loginSchema = z.object({
email: z.string().email('Enter a valid email address'),
password: z.string().min(8, 'Password must be at least 8 characters'),
});
type LoginForm = z.infer<typeof loginSchema>;
export function LoginForm({ onSubmit }: { onSubmit: (data: LoginForm) => void }) {
const { register, handleSubmit, formState: { errors, isSubmitting } } = useForm<LoginForm>({
resolver: zodResolver(loginSchema),
});
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<FormField label="Email" error={errors.email}>
<input type="email" {...register('email')} aria-invalid={!!errors.email} />
</FormField>
<FormField label="Password" error={errors.password}>
<input type="password" {...register('password')} aria-invalid={!!errors.password} />
</FormField>
<button type="submit" disabled={isSubmitting}>
{isSubmitting ? 'Signing in...' : 'Sign In'}
</button>
</form>
);
}
Architecture / How It Works
form-validation-library/
├── schemas/ # Zod validation schemas (auth, profile, checkout, common)
├── components/ # FormField, FormError, FileUpload, MultiStepForm, AsyncSelect
├── hooks/ # useFormPersist, useAsyncValidation, useDependentFields
├── templates/ # LoginForm, RegistrationWizard, CheckoutForm, ContactForm
└── utils/ # Formatting and parsing utilities
Usage Examples
Multi-Step Form Wizard
import { MultiStepForm, Step } from '@/lib/forms/components';
import { personalInfoSchema, addressSchema, paymentSchema } from '@/lib/forms/schemas';
export function CheckoutWizard() {
return (
<MultiStepForm
onComplete={async (data) => {
await fetch('/api/orders', { method: 'POST', body: JSON.stringify(data) });
}}
persistKey="checkout-draft"
>
<Step schema={personalInfoSchema} title="Personal Info">
{({ register, errors }) => (
<>
<FormField label="Full Name" error={errors.name}>
<input {...register('name')} />
</FormField>
<FormField label="Email" error={errors.email}>
<input type="email" {...register('email')} />
</FormField>
</>
)}
</Step>
<Step schema={addressSchema} title="Shipping Address">
{({ register, errors }) => (/* address fields */)}
</Step>
<Step schema={paymentSchema} title="Payment">
{({ register, errors }) => (/* payment fields */)}
</Step>
</MultiStepForm>
);
}
Configuration
Zod Custom Error Messages
// schemas/common.ts — reusable field validators
export const emailField = z
.string()
.min(1, 'Email is required')
.email('Enter a valid email address')
.transform((v) => v.toLowerCase().trim());
export const passwordField = z
.string()
.min(8, 'Must be at least 8 characters')
.regex(/[A-Z]/, 'Must contain an uppercase letter')
.regex(/[0-9]/, 'Must contain a number');
Form Persistence Config
useFormPersist({
key: 'checkout-form',
debounceMs: 500, // save after 500ms of inactivity
storage: sessionStorage, // clear on tab close
exclude: ['password', 'cvv'], // never persist sensitive fields
});
Best Practices
-
Validate on blur, not on change —
mode: 'onBlur'prevents error messages from appearing while the user is still typing -
Use Zod's
.transform()— Normalize data (trim strings, lowercase emails) at the schema level, not in submit handlers - Show errors inline, not in a banner — Users fix errors 40% faster when the message is next to the field
-
Persist multi-step form state — Use
useFormPersistso users don't lose progress on page refresh or accidental navigation - Mark required fields, not optional ones — Most fields are required; mark the exceptions with "(optional)"
-
Disable submit during async validation — Prevent double-submissions and race conditions with
isSubmittingstate
Troubleshooting
| Issue | Cause | Fix |
|---|---|---|
| Zod errors don't show on fields |
zodResolver not connected to useForm
|
Pass resolver: zodResolver(schema) in useForm options |
| File upload doesn't clear after submit | File input value not reset | Call reset() from useForm and clear the file input ref |
| Multi-step form loses state on back navigation | Each step unmounts and remounts | Use useFormPersist with sessionStorage to retain values |
| TypeScript errors on nested form fields |
register path doesn't match schema shape |
Use z.object().merge() or .extend() for nested structures |
This is 1 of 11 resources in the Frontend Developer Pro toolkit. Get the complete [Form Validation Library] with all files, templates, and documentation for $19.
Or grab the entire Frontend Developer Pro bundle (11 products) for $129 — save 30%.
Top comments (0)