Content · Starter
Form Validation
Typed form primitives and three pre-built forms for Next.js 14+ — built on React Hook Form and Zod, with server-side validation via Server Actions and a multi-step form hook.
Read the Getting Access guideif you haven't yet.
1. Get the file
Sign in at marrowstack.dev, open the Form Validation block, and click Copy all files. Paste lib/forms.ts (and the pre-built form components) into your project.
2. Prerequisites
- Next.js 14 or 15, App Router
- TypeScript 5+ with strict mode
- Tailwind CSS (the pre-built form components use Tailwind classes)
3. Install
- Copy the block files into your project.
- Install peer dependencies:
npm install react-hook-form zod @hookform/resolvers4. Wire it in
Use useZodForm() to get a fully-typed React Hook Form instance wired to a Zod schema:
'use client'
import { useZodForm, FormField, FormError } from '@/lib/forms'
import { z } from 'zod'
const schema = z.object({
email: z.string().email(),
message: z.string().min(10),
})
export function ContactForm() {
const { register, handleSubmit, formState: { errors } } = useZodForm(schema)
const onSubmit = handleSubmit(async (data) => {
await fetch('/api/contact', {
method: 'POST',
body: JSON.stringify(data),
})
})
return (
<form onSubmit={onSubmit}>
<FormField label="Email" error={errors.email?.message}>
<input {...register('email')} type="email" />
</FormField>
<FormField label="Message" error={errors.message?.message}>
<textarea {...register('message')} />
</FormField>
<button type="submit">Send</button>
</form>
)
}Or drop in one of the three pre-built forms directly:
import { ContactForm, LoginForm, NewsletterForm } from '@/lib/forms'
// Each form calls a Server Action for server-side validation.
// Wire the action prop to your own handler.
export default function Page() {
return <ContactForm action={submitContactForm} />
}For multi-step flows, use the useMultiStepForm() hook:
'use client'
import { useMultiStepForm } from '@/lib/forms'
const steps = [StepOne, StepTwo, StepThree]
export function OnboardingFlow() {
const { step, next, back, isFirst, isLast } = useMultiStepForm(steps.length)
const CurrentStep = steps[step]
return (
<div>
<CurrentStep />
<div>
{!isFirst && <button onClick={back}>Back</button>}
{!isLast && <button onClick={next}>Next</button>}
</div>
</div>
)
}5. Verify it works
- Render a pre-built form and submit with invalid data — confirm Zod validation errors appear inline.
- Submit with valid data — confirm the Server Action is called and returns without errors.
- Test the multi-step hook by stepping through and confirm the step counter increments and decrements correctly.
6. Failure modes & fixes
Type error on useZodForm — schema type mismatch
Cause: Zod version mismatch between the block and @hookform/resolvers.
Fix: Ensure both use zod@3.x. Run npm ls zod to check for version conflicts.
Server Action not called on submit
Cause: The form is missing the action prop, or the component is used outside a Next.js App Router page.
Fix: Pass your Server Action to the action prop. Server Actions require App Router.
FormField styles not applying
Cause: Tailwind is not scanning the block file for class names.
Fix: Add the block file path to your tailwind.config.ts content array.