Server-side form validation that matches client-side validation — without duplicating logic. Conform bridges the gap with progressive enhancement and type-safe schemas.
What Is Conform?
Conform is a type-safe form validation library for React, built for progressive enhancement. It works with Remix, Next.js, and any React framework — validating on both client and server using the same schema.
Quick Start
npm install @conform-to/react @conform-to/zod zod
import { useForm } from '@conform-to/react';
import { parseWithZod } from '@conform-to/zod';
import { z } from 'zod';
const schema = z.object({
email: z.string().email('Invalid email'),
password: z.string().min(8, 'At least 8 characters'),
});
export function LoginForm() {
const [form, fields] = useForm({
onValidate({ formData }) {
return parseWithZod(formData, { schema });
},
shouldValidate: 'onBlur',
});
return (
<form id={form.id} onSubmit={form.onSubmit} noValidate>
<div>
<label htmlFor={fields.email.id}>Email</label>
<input
id={fields.email.id}
name={fields.email.name}
type="email"
defaultValue={fields.email.initialValue}
/>
<p>{fields.email.errors}</p>
</div>
<div>
<label htmlFor={fields.password.id}>Password</label>
<input
id={fields.password.id}
name={fields.password.name}
type="password"
/>
<p>{fields.password.errors}</p>
</div>
<button type="submit">Log in</button>
</form>
);
}
Server Validation (Remix)
import { parseWithZod } from '@conform-to/zod';
export async function action({ request }) {
const formData = await request.formData();
const submission = parseWithZod(formData, { schema });
if (submission.status !== 'success') {
return submission.reply();
}
// submission.value is typed as { email: string, password: string }
await login(submission.value);
return redirect('/dashboard');
}
Same schema validates on client AND server. No duplication.
Progressive Enhancement
Conform works without JavaScript:
- User submits form → server validates → returns errors
- With JS enabled → client validates before submission
- Same schema, same errors, both paths
Complex Forms
Nested Objects
const schema = z.object({
address: z.object({
street: z.string(),
city: z.string(),
zip: z.string().regex(/^\d{5}$/),
}),
});
Dynamic Arrays
const schema = z.object({
items: z.array(z.object({
name: z.string(),
quantity: z.number().min(1),
})).min(1, 'Add at least one item'),
});
function OrderForm() {
const [form, fields] = useForm({ /* ... */ });
const items = fields.items.getFieldList();
return (
<form>
{items.map((item, index) => {
const itemFields = item.getFieldset();
return (
<fieldset key={item.key}>
<input name={itemFields.name.name} />
<input name={itemFields.quantity.name} type="number" />
<button {...form.remove.getButtonProps({ name: fields.items.name, index })}>
Remove
</button>
</fieldset>
);
})}
<button {...form.insert.getButtonProps({ name: fields.items.name })}>
Add Item
</button>
</form>
);
}
Why Conform
| Feature | Conform | React Hook Form | Formik |
|---|---|---|---|
| Server validation | Built-in | Manual | Manual |
| Progressive enhancement | Yes | No | No |
| Schema validation | Zod/Yup | Resolver | Yup |
| No JS fallback | Works | Broken | Broken |
| FormData native | Yes | Custom | Custom |
| Remix/Next.js | First-class | Plugin | Manual |
Get Started
- Documentation
- GitHub — 2K+ stars
- Examples
Building forms that process web data? My Apify scrapers extract structured data from any site. Custom solutions: spinov001@gmail.com
Top comments (0)