--- name: form-validation-expert description: "Expert guide for complex form handling with React Hook Form, server-side validation (useActionState + Zod), multi-step wizards, and accessible form patterns / Panduan ahli penanganan formulir kompleks dengan React Hook Form, validasi server-side, wizard multi-langkah, dan pola formulir aksesibel." author: "Roedy Rustam" version: "4.4.2" --- # Form & Validation Expert (2026 Edition) [English](#english) | [Bahasa Indonesia](#bahasa-indonesia) --- ## English ### Orchestration & Integration Connects and orchestrates with relevant domain skills: - **`modern-web-guidance`**: Evaluates native form validation features (`:user-valid`, `:user-invalid`), autofill tokens, and custom selects. - **`modern-css-native-expert`**: Leverages `field-sizing: content` for dynamic textareas and pure CSS validation feedback. - **`design-system-architect`**: Provides accessible form control primitives, input borders, and floating label tokens. - **`anti-slop`**: Eliminates vague error messages, double-submission bugs, and unhandled pending states. - **`senior-frontend`**: React 19 `useActionState`, `useOptimistic`, and server action form submission. - **`brainstorming`**, **`zero-to-prod-orchestrator`**, **`session-memory-manager`**: Continuous lifecycle execution. ### Description Production-grade guide for building robust, accessible, and user-friendly forms. Covers **React Hook Form v7+** with Zod/Valibot resolvers, **server-side validation** with `useActionState` + Zod, **multi-step form wizards**, **dynamic forms** from JSON Schema, **autofill-friendly patterns**, **optimistic form submission** with `useOptimistic`, **Conform** (progressive enhancement), and **form accessibility** (ARIA, error announcements). ### Trigger Conditions Activate this skill when: - Building forms with client-side validation (React Hook Form, Zod, Valibot). - Implementing server-side form validation with React 19 Server Actions. - Creating multi-step form wizards with state persistence. - Building dynamic forms generated from schema definitions. - Making forms accessible (ARIA attributes, error announcements). - Implementing file upload forms with drag-and-drop. - Optimizing forms for autofill and mobile usability. --- ### Form Library Selection Guide | Library | Best For | Key Strength | |---|---|---| | **React Hook Form + Zod** | Most React apps | Performance (uncontrolled), rich ecosystem | | **Conform** | Progressive enhancement, RSC | Works without JS, native validation | | **Formik** | Legacy projects | Mature, large community (declining) | | **TanStack Form** | Complex, headless forms | Framework-agnostic, type-safe | **Recommendation**: Use **React Hook Form + Zod** for most projects. Use **Conform** for Next.js Server Actions with progressive enhancement. --- ### 1. React Hook Form + Zod (Client-Side) ```tsx // components/signup-form.tsx 'use client'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; const signupSchema = z.object({ name: z.string().min(2, 'Name must be at least 2 characters'), email: z.string().email('Please enter a valid email'), password: z.string() .min(8, 'Password must be at least 8 characters') .regex(/[A-Z]/, 'Must contain an uppercase letter') .regex(/[0-9]/, 'Must contain a number'), confirmPassword: z.string(), }).refine(data => data.password === data.confirmPassword, { message: 'Passwords do not match', path: ['confirmPassword'], }); type SignupFormData = z.infer; export function SignupForm() { const { register, handleSubmit, formState: { errors, isSubmitting }, } = useForm({ resolver: zodResolver(signupSchema), mode: 'onBlur', // Validate on blur for better UX }); const onSubmit = async (data: SignupFormData) => { const res = await fetch('/api/auth/signup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }); if (!res.ok) throw new Error('Signup failed'); }; return (
{errors.name && }
{errors.email && }
{errors.password && }
{errors.confirmPassword && }
); } ``` --- ### 2. Server Actions + useActionState (React 19 / Next.js 15) ```typescript // actions/signup.ts 'use server'; import { z } from 'zod'; const signupSchema = z.object({ name: z.string().min(2), email: z.string().email(), password: z.string().min(8), }); export type SignupState = { errors?: Record; message?: string; success?: boolean; }; export async function signupAction( prevState: SignupState, formData: FormData, ): Promise { const parsed = signupSchema.safeParse({ name: formData.get('name'), email: formData.get('email'), password: formData.get('password'), }); if (!parsed.success) { return { errors: parsed.error.flatten().fieldErrors as Record, message: 'Please fix the errors below.', }; } // Check if email already exists const existing = await db.query.users.findFirst({ where: eq(users.email, parsed.data.email), }); if (existing) { return { errors: { email: ['This email is already registered'] } }; } // Create user await createUser(parsed.data); return { success: true, message: 'Account created!' }; } ``` ```tsx // components/signup-form-server.tsx 'use client'; import { useActionState } from 'react'; import { signupAction, type SignupState } from '@/actions/signup'; export function SignupFormServer() { const [state, formAction, isPending] = useActionState( signupAction, {}, ); return (
{state.message && (
{state.message}
)}
{state.errors?.name &&

{state.errors.name[0]}

}
{state.errors?.email &&

{state.errors.email[0]}

}
{state.errors?.password &&

{state.errors.password[0]}

}
); } ``` --- ### 3. Multi-Step Form Wizard ```tsx // components/multi-step-form.tsx 'use client'; import { useState, useCallback } from 'react'; import { useForm, FormProvider, type UseFormReturn } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; import { z } from 'zod'; // Step schemas const step1Schema = z.object({ companyName: z.string().min(1, 'Required'), industry: z.string().min(1, 'Required'), }); const step2Schema = z.object({ contactName: z.string().min(1, 'Required'), contactEmail: z.string().email(), }); const step3Schema = z.object({ plan: z.enum(['starter', 'pro', 'enterprise']), billingCycle: z.enum(['monthly', 'annual']), }); const fullSchema = step1Schema.merge(step2Schema).merge(step3Schema); type FormData = z.infer; const STEP_SCHEMAS = [step1Schema, step2Schema, step3Schema] as const; const STEP_TITLES = ['Company Info', 'Contact Details', 'Choose Plan'] as const; export function MultiStepForm() { const [currentStep, setCurrentStep] = useState(0); const methods = useForm({ resolver: zodResolver(fullSchema), mode: 'onChange', }); const goNext = useCallback(async () => { const schema = STEP_SCHEMAS[currentStep]; const fields = Object.keys(schema.shape) as (keyof FormData)[]; const isValid = await methods.trigger(fields); if (isValid) setCurrentStep(s => Math.min(s + 1, STEP_SCHEMAS.length - 1)); }, [currentStep, methods]); const goBack = useCallback(() => { setCurrentStep(s => Math.max(s - 1, 0)); }, []); const onSubmit = async (data: FormData) => { await fetch('/api/onboarding', { method: 'POST', body: JSON.stringify(data), }); }; return ( {/* Progress indicator */}
{currentStep === 0 && } {currentStep === 1 && } {currentStep === 2 && }
{currentStep > 0 && } {currentStep < STEP_SCHEMAS.length - 1 ? : }
); } ``` --- ### 4. Form Accessibility Checklist | Requirement | Implementation | |---|---| | **Labels** | Every input has a visible `