Type-safe form validation utilizing web fundamentals using Conform and Zod (or Valibot).
Seamless form submission experience out of the box
Clean, declarative API
Type-safety powered by Zod
Single validation source for client & server using Zod
Built-in accessibility and keyboard navigation
Field auto-focus on validation error
Optimized for React 19 / Next 15 and Server Components
Prevents automatic form reset after submission
Zod error messages animated with Motion
Progressive enhancement–first APIs
The usage consists of the following steps:
The hook is a React hook that integrates Zod schema validation with form handling using the Conform library.
It’s designed for use with RSC, specifically leveraging . It accepts a Zod schema, an optional submission result, and an action. The hook ensures validation happens on blur and revalidation occurs on input, preventing the default form reset behavior while keeping the last submission result in sync.
Essentially, it's a wrapper around the Conform hook, which removes some boilerplate and provides a more convenient API and sensible defaults. You can use Conform hook instead if you prefer.
The hook returns a tuple containing and metadata that you can use to enhance a HTML form by using and components with the provided metadata.
Uses Conform’s and sets all props required to make a form element accessible. To function properly, pass the metadata returned by hook as the prop.
To enhance the developer experience, additional form field components are provided that wrap inputs, adding styling, error messages, labels, and Conform’s meta binding.
To ensure proper functionality, pass the appropriate field from the metadata returned by the hook as the prop.
Prop | Type | Default |
---|---|---|
size | enum | "default" |
radius | enum | ― |
invalid | boolean | ― |
reduceMotion | boolean | false |
validate | FieldMetadata<string | boolean | undefined> | ― |
label | ReactNode | string | ― |
Prop | Type | Default |
---|---|---|
type | enum | "text" |
size | enum | "default" |
radius | enum | ― |
invalid | boolean | ― |
reduceMotion | boolean | false |
validate | FieldMetadata<string | boolean | undefined> | ― |
label | ReactNode | string | ― |
Prop | Type | Default |
---|---|---|
size | enum | "default" |
radius | enum | ― |
invalid | boolean | ― |
reduceMotion | boolean | false |
minRows | number | 2 |
maxRows | number | ― |
resize | enum | ― |
validate | FieldMetadata<string | boolean | undefined> | ― |
label | ReactNode | string | ― |
Prop | Type | Default |
---|---|---|
size | enum | "default" |
invalid | boolean | ― |
reduceMotion | boolean | false |
options* | RadioOption[] | ― |
validate | FieldMetadata<string | boolean | undefined> | ― |
label | ReactNode | string | ― |
Prop | Type | Default |
---|---|---|
options* | OptionDataProps[] | ― |
groupedOptions* | GroupedOptionDataProps[] | ― |
size | enum | "default" |
radius | enum | ― |
reduceMotion | boolean | false |
indicator | ReactNode | ― |
animation | AnimationProps | ― |
animationPreset | enum | ― |
transition | Transition | ― |
transitionPreset | enum TransitionPreset | ― |
placeholder | ReactNode | ― |
icon | ReactNode | ― |
invalid | boolean | ― |
validate | FieldMetadata<string | boolean | undefined> | ― |
label | ReactNode | string | ― |
Prop | Type | Default |
---|---|---|
size | enum | "default" |
invalid | boolean | ― |
elastic | boolean | ― |
reduceMotion | boolean | false |
validate | FieldMetadata<string | boolean | undefined> | ― |
label | ReactNode | string | ― |
You’re not limited to using only components. If you need more control over component rendering, you can use any input component with validation:
You can even use native HTML inputs:
If you don’t need , , or error messages, you can use form inputs wrapped with Conform directly.
Prop | Type | Default |
---|---|---|
size | enum | "default" |
radius | enum | ― |
invalid | boolean | ― |
reduceMotion | boolean | false |
validate | FieldMetadata<string | boolean | undefined> | ― |
Prop | Type | Default |
---|---|---|
type | enum | "text" |
size | enum | "default" |
radius | enum | ― |
invalid | boolean | ― |
reduceMotion | boolean | false |
validate | FieldMetadata<string | boolean | undefined> | ― |
Prop | Type | Default |
---|---|---|
size | enum | "default" |
invalid | boolean | ― |
reduceMotion | boolean | false |
options* | RadioOption[] | ― |
validate | FieldMetadata<string | boolean | undefined> | ― |
Prop | Type | Default |
---|---|---|
options* | OptionDataProps[] | ― |
groupedOptions* | GroupedOptionDataProps[] | ― |
size | enum | "default" |
radius | enum | ― |
reduceMotion | boolean | false |
indicator | ReactNode | ― |
animation | AnimationProps | ― |
animationPreset | enum | ― |
transition | Transition | ― |
transitionPreset | enum TransitionPreset | ― |
placeholder | ReactNode | ― |
icon | ReactNode | ― |
invalid | boolean | ― |
validate | FieldMetadata<string | boolean | undefined> | ― |
Prop | Type | Default |
---|---|---|
size | enum | "default" |
invalid | boolean | ― |
elastic | boolean | ― |
reduceMotion | boolean | false |
validate | FieldMetadata<string | boolean | undefined> | ― |
Prop | Type | Default |
---|---|---|
size | enum | "default" |
radius | enum | ― |
invalid | boolean | ― |
reduceMotion | boolean | false |
minRows | number | 2 |
maxRows | number | ― |
resize | enum | ― |
validate | FieldMetadata<string | boolean | undefined> | ― |
Prop | Type | Default |
---|---|---|
validate* | FormMetadata<input<T>, string[]> | ― |
invalidAnimation | enum | ― |
Prop | Type | Default |
---|---|---|
legend | ReactNode | string | ― |
messages | string[] | ― |
size | enum | "default" |
Prop | Type | Default |
---|---|---|
messages | string[] | ― |
size | enum | "default" |
Prop | Type | Default |
---|---|---|
messages | string[] | ― |
size | enum | "default" |
useForm
Form
*Field
parseWithZod
useForm
useActionState
"use client"
import { useActionState } from "react"
import { useForm } from "@/hooks/use-form"
import { FORM_VALIDATION_ACTION } from "./actions"
import { ZodFormValidationSchema } from "./schema"
export default function ValidationDemo() {
const [actionResult, action, pending] = useActionState(
FORM_VALIDATION_ACTION,
undefined,
)
const [form, fields] = useForm({
actionResult,
action,
schema: ZodFormValidationSchema,
})
return ...
}
useForm
form
fields
Form
*Field
getFormProps
form
useForm
validate
export default function ValidationDemo() {
const [form, fields] = useForm(...)
return (
<Form validate={form}>
...
</Form>
)
}
fields
useForm
validate
...
import { ZodFormValidationSchema } from "./schema"
export default function ValidationDemo() {
...
const [form, fields] = useForm({
schema: ZodFormValidationSchema,
...
})
return (
<Form validate={form}>
<InputField validate={fields.email} />
</Form>
)
}
*Field
Label
FormField