--- name: form-design description: Build accessible, user-friendly forms with validation. Covers react-hook-form, Zod schemas, error handling UX, multi-step forms, input patterns, and form accessibility. Use for registration forms, checkout flows, data entry, and user input. --- # Form Design & Validation Create accessible, validated forms with excellent user experience. ## Instructions 1. **Use proper labels** - Every input needs an associated label 2. **Validate on blur and submit** - Immediate feedback without being intrusive 3. **Show clear error messages** - Specific, actionable guidance 4. **Group related fields** - Use fieldsets for logical groupings 5. **Support keyboard navigation** - Tab order, Enter to submit ## React Hook Form + Zod (Recommended Stack) ### Setup ```bash npm install react-hook-form zod @hookform/resolvers ``` ### Basic Form ```tsx import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; // 1. Define schema const signupSchema = z.object({ email: z .string() .min(1, 'Email is required') .email('Please enter a valid email'), password: z .string() .min(8, 'Password must be at least 8 characters') .regex(/[A-Z]/, 'Password must contain an uppercase letter') .regex(/[0-9]/, 'Password must contain a number'), confirmPassword: z.string(), }).refine((data) => data.password === data.confirmPassword, { message: "Passwords don't match", path: ['confirmPassword'], }); type SignupForm = z.infer; // 2. Create form function SignupForm() { const { register, handleSubmit, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(signupSchema), }); const onSubmit = async (data: SignupForm) => { try { await api.signup(data); // Handle success } catch (error) { // Handle error } }; return (
); } ``` ### Reusable Form Field Component ```tsx import { forwardRef } from 'react'; interface FormFieldProps extends React.InputHTMLAttributes { label: string; error?: string; hint?: string; } export const FormField = forwardRef( ({ label, error, hint, id, type = 'text', ...props }, ref) => { const inputId = id || label.toLowerCase().replace(/\s+/g, '-'); const errorId = `${inputId}-error`; const hintId = `${inputId}-hint`; return (
{hint && !error && (

{hint}

)} {error && ( )}
); } ); ``` ## Common Form Patterns ### Select/Dropdown ```tsx interface SelectFieldProps { label: string; options: { value: string; label: string }[]; error?: string; placeholder?: string; } export const SelectField = forwardRef( ({ label, options, error, placeholder, ...props }, ref) => { const id = label.toLowerCase().replace(/\s+/g, '-'); return (
{error &&

{error}

}
); } ); ``` ### Checkbox Group ```tsx interface CheckboxGroupProps { label: string; options: { value: string; label: string }[]; value: string[]; onChange: (value: string[]) => void; error?: string; } function CheckboxGroup({ label, options, value, onChange, error }: CheckboxGroupProps) { const handleChange = (optionValue: string, checked: boolean) => { if (checked) { onChange([...value, optionValue]); } else { onChange(value.filter(v => v !== optionValue)); } }; return (
{label}
{options.map(opt => ( ))}
{error &&

{error}

}
); } ``` ### Radio Group ```tsx interface RadioGroupProps { label: string; options: { value: string; label: string; description?: string }[]; value: string; onChange: (value: string) => void; error?: string; } function RadioGroup({ label, options, value, onChange, error }: RadioGroupProps) { return (
{label}
{options.map(opt => ( ))}
{error &&

{error}

}
); } ``` ## Multi-Step Form ```tsx import { useState } from 'react'; import { useForm, FormProvider } from 'react-hook-form'; const steps = [ { id: 'account', title: 'Account', component: AccountStep }, { id: 'profile', title: 'Profile', component: ProfileStep }, { id: 'preferences', title: 'Preferences', component: PreferencesStep }, ]; function MultiStepForm() { const [currentStep, setCurrentStep] = useState(0); const methods = useForm({ mode: 'onChange' }); const CurrentStepComponent = steps[currentStep].component; const isLastStep = currentStep === steps.length - 1; const next = async () => { const isValid = await methods.trigger(); // Validate current step if (isValid && !isLastStep) { setCurrentStep(prev => prev + 1); } }; const back = () => { if (currentStep > 0) { setCurrentStep(prev => prev - 1); } }; const onSubmit = async (data: FormData) => { await api.submit(data); }; return (
{/* Progress indicator */} {/* Current step */} {/* Navigation */}
{isLastStep ? ( ) : ( )}
); } ``` ## Error Handling Patterns ### Inline Errors ```tsx // Show error immediately below field {error && (

{error}

)} ``` ### Error Summary ```tsx // Show all errors at top of form function ErrorSummary({ errors }: { errors: Record }) { const errorList = Object.entries(errors).filter(([_, v]) => v.message); if (errorList.length === 0) return null; return (

Please fix the following errors:

); } ``` ## Accessibility Checklist - [ ] Every input has a `