The astro-basics project includes a comprehensive form validation system that combines React Hook Form for state management, Zod for schema validation, and @fpkit/acss for accessible form components.
Overview
Section titled “Overview”This integration provides type-safe, accessible forms with minimal boilerplate and optimized performance.
Schema-Driven
Define validation once in Zod, automatically infer TypeScript types.
Accessible
@fpkit/acss components provide ARIA attributes and keyboard navigation.
Performant
React Hook Form minimizes re-renders - only validates on submit/blur.
Type-Safe
Full TypeScript support with inferred types from schemas.
Quick Start
Section titled “Quick Start”30-Second Example
Section titled “30-Second Example”import { useForm } from 'react-hook-form'import { zodResolver } from '@hookform/resolvers/zod'import { z } from 'zod'import { FormField } from '#components/react/form/FormField'import { Button } from '@fpkit/acss'
const schema = z.object({ email: z.string().email('Invalid email'),})
type FormData = z.infer<typeof schema>
function MyForm() { const { register, handleSubmit, formState: { errors }, } = useForm<FormData>({ resolver: zodResolver(schema), })
return ( <form onSubmit={handleSubmit(data => console.log(data))}> <FormField id="email" label="Email" type="email" error={errors.email?.message} registration={register('email')} required /> <Button type="submit">Submit</Button> </form> )}Available Components
Section titled “Available Components”File Structure
Section titled “File Structure”src/├── schemas/│ └── client.schema.ts # Zod validation schemas└── components/react/ ├── form/ │ ├── FormField.tsx # Reusable field component │ └── index.ts # Barrel export └── ClientFormRHF.tsx # Complete form exampleFormField Component
Section titled “FormField Component”A reusable form field that wraps @fpkit/acss components with React Hook Form integration.
export type Props = { id: string // Unique field identifier label: string // Label text type?: 'text' | 'email' | 'tel' | 'password' | 'url' | 'number' | 'textarea' placeholder?: string // Placeholder text error?: string // Error message from validation registration: UseFormRegisterReturn // React Hook Form register() return required?: boolean // Required field indicator disabled?: boolean // Disable the field rows?: number // Rows for textarea (default: 5) hintText?: string // Additional hint text className?: string // Additional CSS classes}import { FormField } from '#components/react/form/FormField'
<FormField id="email" label="Email Address" type="email" placeholder="Enter your email" error={errors.email?.message} registration={register('email')} required/>Creating Form Schemas
Section titled “Creating Form Schemas”Basic Schema
Section titled “Basic Schema”Create schemas in src/schemas/ using Zod:
import { z } from 'zod'
export const loginSchema = z.object({ email: z.string().min(1, 'Email is required').email('Invalid email address'), password: z.string().min(8, 'Password must be at least 8 characters'),})
export type LoginFormData = z.infer<typeof loginSchema>Defining Error Messages
Section titled “Defining Error Messages”Pass clear, user-facing messages inline to each validator:
import { z } from 'astro/zod'
export const clientSchema = z.object({ nickname: z.string().min(1, 'Full name or preferred name is required'), email: z .string() .email({ error: 'Please enter a valid email address' }) .optional() .or(z.literal('')), phone: z .string() .optional() .refine(val => !val || /^[\d\s-()]{7,}$/.test(val), { message: 'Please enter a valid phone number', }), gender: z.enum(['male', 'female', 'other', 'prefer_not_to_say']),})
export type ClientFormData = z.infer<typeof clientSchema>Input Types
Section titled “Input Types”The FormField component supports multiple input types:
<FormField id="name" label="Full Name" type="text" error={errors.name?.message} registration={register('name')}/><FormField id="email" label="Email Address" type="email" error={errors.email?.message} registration={register('email')}/><FormField id="password" label="Password" type="password" hintText="Must be at least 8 characters" error={errors.password?.message} registration={register('password')}/><FormField id="message" label="Your Message" type="textarea" rows={7} error={errors.message?.message} registration={register('message')}/>import { Controller } from 'react-hook-form'import { Checkbox } from '@fpkit/acss'
<Controller name="veteran_status" control={control} render={({ field }) => ( <Checkbox id="veteran_status" label="Veteran Status" checked={field.value ?? false} onChange={field.onChange} size="lg" /> )}/>{errors.veteran_status && ( <p className="form-error" role="alert"> {errors.veteran_status.message} </p>)}Using in Astro Pages
Section titled “Using in Astro Pages”To use forms in Astro pages, add client:load for hydration:
---import Layout from '#layouts/Base.astro'import ClientFormRHF from '#components/react/ClientFormRHF'import { generateCSRFToken } from '#utils/csrf'
const csrfToken = generateCSRFToken()---
<Layout title="New Client"> <main> <h1>New Client</h1> <ClientFormRHF client:load csrfToken={csrfToken} /> </main></Layout>Best Practices
Section titled “Best Practices”-
Always infer types from schemas
// Good - type derived from schematype FormData = z.infer<typeof schema>// Bad - manual type definitioninterface FormData {email: string} -
Write clear, user-facing error messages inline
z.string().min(1, 'We need your email to get back to you') -
Provide default values
useForm<FormData>({resolver: zodResolver(schema),defaultValues: { name: '', email: '' },}) -
Use path aliases for imports
import { FormField } from '#components/react/form/FormField'import { clientSchema } from '#schemas/client.schema'
Complete Example
Section titled “Complete Example”See the full implementation in:
- Schema:
src/schemas/client.schema.ts - FormField:
src/components/react/form/FormField.tsx - ClientFormRHF:
src/components/react/ClientFormRHF.tsx
For detailed documentation including advanced patterns, multi-step forms, and troubleshooting, see:
Full Guide: project-docs/02-guides/react-hook-form-zod-guide.md