This skill should be used when generating React forms with React Hook Form, Zod validation, and shadcn/ui components. Applies when creating entity forms, character editors, location forms, data entry forms, or any form requiring client and server validation. Trigger terms include create form, generate form, build form, React Hook Form, RHF, Zod validation, form component, entity form, character form, data entry, form schema.
Generates production-ready React forms with React Hook Form, Zod validation, and shadcn/ui components. Trigger with terms like "create form" or "generate form" for entity forms, data entry, or any form requiring client/server validation.
/plugin marketplace add hopeoverture/worldbuilding-app-skills/plugin install form-generator-rhf-zod@worldbuilding-app-skillsThis skill inherits all available tools. When active, it can use any tool Claude has access to.
assets/form-template.tsxreferences/rhf-patterns.mdreferences/zod-validation.mdGenerate production-ready React forms using React Hook Form, Zod validation schemas, and accessible shadcn/ui form controls. This skill creates forms with client-side and server-side validation, proper TypeScript types, and consistent error handling.
Apply this skill when:
scripts/generate_form.py - Generates form component, Zod schema, and server action from field specifications.
Usage:
python scripts/generate_form.py --name CharacterForm --fields fields.json --output components/forms
scripts/generate_zod_schema.py - Converts field specifications to Zod schema with validation rules.
Usage:
python scripts/generate_zod_schema.py --fields fields.json --output lib/schemas
references/rhf-patterns.md - React Hook Form patterns, hooks, and best practices references/zod-validation.md - Zod schema patterns, refinements, and custom validators references/shadcn-form-controls.md - shadcn/ui form component usage and examples references/server-actions.md - Server action patterns for form submission
assets/form-template.tsx - Base form component template with RHF setup assets/field-templates/ - Individual field component templates (Input, Textarea, Select, Checkbox, etc.) assets/validation-schemas.ts - Common Zod validation patterns assets/form-utils.ts - Form utility functions (formatters, transformers, validators)
Create a field specification file describing form fields, types, validation rules, and UI properties.
Field specification format:
{
"fields": [
{
"name": "characterName",
"label": "Character Name",
"type": "text",
"required": true,
"validation": {
"minLength": 2,
"maxLength": 100,
"pattern": "^[a-zA-Z\\s'-]+$"
},
"placeholder": "Enter character name",
"helpText": "The character's full name as it appears in your world"
},
{
"name": "age",
"label": "Age",
"type": "number",
"required": false,
"validation": {
"min": 0,
"max": 10000
}
},
{
"name": "faction",
"label": "Faction",
"type": "select",
"required": true,
"options": "dynamic",
"optionsSource": "api.getFactions()"
},
{
"name": "biography",
"label": "Biography",
"type": "textarea",
"required": false,
"validation": {
"maxLength": 5000
},
"rows": 8
}
],
"formOptions": {
"submitLabel": "Create Character",
"resetLabel": "Clear Form",
"showReset": true,
"successMessage": "Character created successfully",
"errorMessage": "Failed to create character"
}
}
Use scripts/generate_zod_schema.py to create type-safe validation schema:
python scripts/generate_zod_schema.py --fields character-fields.json --output lib/schemas/character.ts
Generated schema includes:
Use scripts/generate_form.py to create React Hook Form component:
python scripts/generate_form.py --name CharacterForm --fields character-fields.json --output components/forms
Generated component includes:
Generate server action for form submission with server-side validation:
'use server'
import { z } from 'zod'
import { characterSchema } from '@/lib/schemas/character'
import { createCharacter } from '@/lib/db/characters'
export async function createCharacterAction(data: z.infer<typeof characterSchema>) {
// Server-side validation
const validated = characterSchema.safeParse(data)
if (!validated.success) {
return {
success: false,
errors: validated.error.flatten().fieldErrors
}
}
// Database operation
const character = await createCharacter(validated.data)
return {
success: true,
data: character
}
}
Import and use generated form component in page or parent component:
import { CharacterForm } from '@/components/forms/CharacterForm'
export default function CreateCharacterPage() {
return (
<div className="container max-w-2xl py-8">
<h1 className="text-3xl font-bold mb-6">Create New Character</h1>
<CharacterForm />
</div>
)
}
Supported field types and their shadcn/ui mappings:
Common validation patterns using Zod:
// Required with length constraints
z.string().min(2, "Too short").max(100, "Too long")
// Email
z.string().email("Invalid email")
// URL
z.string().url("Invalid URL")
// Pattern matching
z.string().regex(/^[a-zA-Z]+$/, "Letters only")
// Trimmed strings
z.string().trim().min(1)
// Custom transformation
z.string().transform(val => val.toLowerCase())
// Range validation
z.number().min(0).max(100)
// Integer only
z.number().int("Must be whole number")
// Positive numbers
z.number().positive("Must be positive")
// Custom refinement
z.number().refine(val => val % 5 === 0, "Must be multiple of 5")
// Array with min/max items
z.array(z.string()).min(1, "Select at least one").max(5, "Too many")
// Non-empty array
z.array(z.string()).nonempty("Required")
// Nested objects
z.object({
address: z.object({
street: z.string(),
city: z.string(),
zipCode: z.string().regex(/^\d{5}$/)
})
})
// Refine with cross-field validation
z.object({
password: z.string().min(8),
confirmPassword: z.string()
}).refine(data => data.password === data.confirmPassword, {
message: "Passwords must match",
path: ["confirmPassword"]
})
// Optional (can be undefined)
z.string().optional()
// Nullable (can be null)
z.string().nullable()
// Optional with default
z.string().default("default value")
'use client'
import { useForm } from 'react-hook-form'
import { zodResolver } from '@hookform/resolvers/zod'
import { z } from 'zod'
import { Button } from '@/components/ui/button'
import {
Form,
FormControl,
FormDescription,
FormField,
FormItem,
FormLabel,
FormMessage,
} from '@/components/ui/form'
import { Input } from '@/components/ui/input'
import { toast } from 'sonner'
const formSchema = z.object({
name: z.string().min(2).max(100),
email: z.string().email()
})
type FormValues = z.infer<typeof formSchema>
export function ExampleForm() {
const form = useForm<FormValues>({
resolver: zodResolver(formSchema),
defaultValues: {
name: '',
email: ''
}
})
async function onSubmit(values: FormValues) {
try {
const result = await submitAction(values)
if (result.success) {
toast.success('Submitted successfully')
form.reset()
} else {
toast.error(result.message)
}
} catch (error) {
toast.error('An error occurred')
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Enter name" {...field} />
</FormControl>
<FormDescription>Your display name</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input type="email" placeholder="you@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="submit" disabled={form.formState.isSubmitting}>
{form.formState.isSubmitting ? 'Submitting...' : 'Submit'}
</Button>
</form>
</Form>
)
}
import { useFieldArray } from 'react-hook-form'
import { Button } from '@/components/ui/button'
// In schema
const formSchema = z.object({
tags: z.array(z.object({
value: z.string().min(1)
})).min(1)
})
// In component
const { fields, append, remove } = useFieldArray({
control: form.control,
name: 'tags'
})
// In JSX
{fields.map((field, index) => (
<div key={field.id} className="flex gap-2">
<FormField
control={form.control}
name={`tags.${index}.value`}
render={({ field }) => (
<FormItem className="flex-1">
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button type="button" variant="destructive" size="icon" onClick={() => remove(index)}>
X
</Button>
</div>
))}
<Button type="button" onClick={() => append({ value: '' })}>
Add Tag
</Button>
const [preview, setPreview] = useState<string | null>(null)
<FormField
control={form.control}
name="avatar"
render={({ field: { value, onChange, ...field } }) => (
<FormItem>
<FormLabel>Avatar</FormLabel>
<FormControl>
<Input
type="file"
accept="image/*"
{...field}
onChange={(e) => {
const file = e.target.files?.[0]
if (file) {
onChange(file)
const reader = new FileReader()
reader.onloadend = () => setPreview(reader.result as string)
reader.readAsDataURL(file)
}
}}
/>
</FormControl>
{preview && (
<img src={preview} alt="Preview" className="mt-2 h-32 w-32 object-cover rounded" />
)}
<FormMessage />
</FormItem>
)}
/>
const showAdvanced = form.watch('showAdvanced')
<FormField
control={form.control}
name="showAdvanced"
render={({ field }) => (
<FormItem className="flex items-center gap-2">
<FormControl>
<Switch checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<FormLabel>Show Advanced Options</FormLabel>
</FormItem>
)}
/>
{showAdvanced && (
<FormField
control={form.control}
name="advancedOption"
render={({ field }) => (
<FormItem>
<FormLabel>Advanced Option</FormLabel>
<FormControl>
<Input {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
)}
Ensure forms are accessible by:
Test forms using React Testing Library and Vitest:
import { render, screen, waitFor } from '@testing-library/react'
import userEvent from '@testing-library/user-event'
import { CharacterForm } from './CharacterForm'
describe('CharacterForm', () => {
it('validates required fields', async () => {
render(<CharacterForm />)
const submitButton = screen.getByRole('button', { name: /submit/i })
await userEvent.click(submitButton)
expect(await screen.findByText(/name is required/i)).toBeInTheDocument()
})
it('submits valid data', async () => {
const mockSubmit = vi.fn()
render(<CharacterForm onSubmit={mockSubmit} />)
await userEvent.type(screen.getByLabelText(/name/i), 'Aragorn')
await userEvent.click(screen.getByRole('button', { name: /submit/i }))
await waitFor(() => {
expect(mockSubmit).toHaveBeenCalledWith({
name: 'Aragorn'
})
})
})
})
Fields: name, race, faction, class, age, appearance, biography, relationships, attributes, inventory
Fields: name, type, region, coordinates, climate, population, government, description, points of interest
Fields: name, type, rarity, owner, location, properties, history, magical effects, value
Fields: title, date, location, participants, description, consequences, related events
Fields: name, type, leader, headquarters, goals, allies, enemies, members, history
When generating forms, ensure:
Ensure these packages are installed:
npm install react-hook-form @hookform/resolvers zod
npm install sonner # for toast notifications
shadcn/ui components needed:
npx shadcn-ui@latest add form button input textarea select checkbox radio-group switch slider
z.infer<typeof schema> for TypeScript typesIssue: Form not submitting
Issue: Validation not working
Issue: TypeScript errors
Issue: Field not updating
Consult references/ directory for detailed patterns:
Use assets/ directory for starting templates:
Use when working with Payload CMS projects (payload.config.ts, collections, fields, hooks, access control, Payload API). Use when debugging validation errors, security issues, relationship queries, transactions, or hook behavior.