Claude-skill-registry conform
Builds progressive enhancement forms with Conform using web standards, server validation, and framework integration. Use when building Remix/Next.js forms, implementing progressive enhancement, or needing accessible form validation.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/conform" ~/.claude/skills/majiayu000-claude-skill-registry-conform && rm -rf "$T"
manifest:
skills/data/conform/SKILL.mdsource content
Conform
Type-safe form validation library with progressive enhancement for Remix and Next.js.
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(), password: z.string().min(8), }); export default function LoginForm() { const [form, fields] = useForm({ onValidate({ formData }) { return parseWithZod(formData, { schema }); }, }); return ( <form id={form.id} onSubmit={form.onSubmit}> <input name={fields.email.name} defaultValue={fields.email.initialValue} /> <div>{fields.email.errors}</div> <input name={fields.password.name} type="password" defaultValue={fields.password.initialValue} /> <div>{fields.password.errors}</div> <button>Submit</button> </form> ); }
Remix Integration
Action & Loader
import { json, redirect } from '@remix-run/node'; import { useActionData } from '@remix-run/react'; 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 async function action({ request }: ActionFunctionArgs) { const formData = await request.formData(); const submission = parseWithZod(formData, { schema }); if (submission.status !== 'success') { return json(submission.reply()); } // Process valid data await createUser(submission.value); return redirect('/dashboard'); } export default function SignUp() { const lastResult = useActionData<typeof action>(); const [form, fields] = useForm({ lastResult, onValidate({ formData }) { return parseWithZod(formData, { schema }); }, shouldValidate: 'onBlur', shouldRevalidate: 'onInput', }); return ( <form method="post" id={form.id} onSubmit={form.onSubmit}> <label> Email <input name={fields.email.name} /> <div>{fields.email.errors}</div> </label> <label> Password <input name={fields.password.name} type="password" /> <div>{fields.password.errors}</div> </label> <button>Sign Up</button> </form> ); }
Next.js Integration (App Router)
Server Action
'use server'; import { parseWithZod } from '@conform-to/zod'; import { z } from 'zod'; const schema = z.object({ email: z.string().email(), password: z.string().min(8), }); export async function login(prevState: unknown, formData: FormData) { const submission = parseWithZod(formData, { schema }); if (submission.status !== 'success') { return submission.reply(); } // Authenticate user const user = await authenticate(submission.value); if (!user) { return submission.reply({ formErrors: ['Invalid credentials'], }); } redirect('/dashboard'); }
Client Component
'use client'; import { useFormState } from 'react-dom'; import { useForm } from '@conform-to/react'; import { parseWithZod } from '@conform-to/zod'; import { login } from './actions'; export function LoginForm() { const [lastResult, action] = useFormState(login, undefined); const [form, fields] = useForm({ lastResult, onValidate({ formData }) { return parseWithZod(formData, { schema }); }, }); return ( <form id={form.id} action={action} onSubmit={form.onSubmit}> <input name={fields.email.name} /> <div>{fields.email.errors}</div> <input name={fields.password.name} type="password" /> <div>{fields.password.errors}</div> <button>Login</button> </form> ); }
Form Configuration
useForm Options
const [form, fields] = useForm({ // Last server result lastResult, // Client-side validation onValidate({ formData }) { return parseWithZod(formData, { schema }); }, // When to validate shouldValidate: 'onBlur', // 'onSubmit' | 'onBlur' | 'onInput' shouldRevalidate: 'onInput', // When to re-validate after error // Initial values defaultValue: { email: '', password: '', }, // Constraint validation (HTML5) constraint: getZodConstraint(schema), });
Form Props
<form id={form.id} onSubmit={form.onSubmit} noValidate // Disable browser validation > {/* Form errors */} {form.errors && <div>{form.errors}</div>} </form>
Field Helpers
getInputProps
import { getInputProps } from '@conform-to/react'; <input {...getInputProps(fields.email, { type: 'email', ariaAttributes: true, })} /> // Generates: // name, id, defaultValue, aria-invalid, aria-describedby
getTextareaProps
import { getTextareaProps } from '@conform-to/react'; <textarea {...getTextareaProps(fields.message)} />
getSelectProps
import { getSelectProps } from '@conform-to/react'; <select {...getSelectProps(fields.country)}> <option value="">Select country</option> <option value="us">United States</option> </select>
Checkbox/Radio
import { getInputProps } from '@conform-to/react'; <input {...getInputProps(fields.remember, { type: 'checkbox' })} /> <input {...getInputProps(fields.plan, { type: 'radio', value: 'free' })} /> <input {...getInputProps(fields.plan, { type: 'radio', value: 'pro' })} />
Nested Objects
const schema = z.object({ user: z.object({ name: z.string(), email: z.string().email(), }), address: z.object({ street: z.string(), city: z.string(), }), }); function Form() { const [form, fields] = useForm({ onValidate({ formData }) { return parseWithZod(formData, { schema }); }, }); const user = fields.user.getFieldset(); const address = fields.address.getFieldset(); return ( <form> <input name={user.name.name} /> <input name={user.email.name} /> <input name={address.street.name} /> <input name={address.city.name} /> </form> ); }
Field Arrays
import { useForm, useFieldList, insert, remove } from '@conform-to/react'; const schema = z.object({ tasks: z.array(z.object({ title: z.string(), done: z.boolean().default(false), })), }); function TaskForm() { const [form, fields] = useForm({ onValidate({ formData }) { return parseWithZod(formData, { schema }); }, defaultValue: { tasks: [{ title: '', done: false }], }, }); const tasks = fields.tasks.getFieldList(); return ( <form id={form.id} onSubmit={form.onSubmit}> {tasks.map((task, index) => { const taskFields = task.getFieldset(); return ( <div key={task.key}> <input name={taskFields.title.name} /> <input type="checkbox" name={taskFields.done.name} value="true" /> <button {...form.remove.getButtonProps({ name: fields.tasks.name, index, })} > Remove </button> </div> ); })} <button {...form.insert.getButtonProps({ name: fields.tasks.name, defaultValue: { title: '', done: false }, })} > Add Task </button> <button>Submit</button> </form> ); }
Intent Buttons
Handle different submit actions:
<form> <button name="intent" value="save"> Save Draft </button> <button name="intent" value="publish"> Publish </button> </form>
// In action export async function action({ request }) { const formData = await request.formData(); const intent = formData.get('intent'); if (intent === 'save') { // Save draft } else if (intent === 'publish') { // Publish } }
File Uploads
const schema = z.object({ avatar: z.instanceof(File) .refine((file) => file.size < 5_000_000, 'Max 5MB'), }); function AvatarForm() { const [form, fields] = useForm({ onValidate({ formData }) { return parseWithZod(formData, { schema }); }, }); return ( <form method="post" encType="multipart/form-data"> <input type="file" name={fields.avatar.name} accept="image/*" /> <div>{fields.avatar.errors}</div> </form> ); }
Accessibility
Conform automatically handles accessibility:
// Using getInputProps with ariaAttributes <input {...getInputProps(fields.email, { type: 'email', ariaAttributes: true, })} /> // Generates: // aria-invalid="true" when has errors // aria-describedby pointing to error element // Error element <div id={fields.email.errorId}>{fields.email.errors}</div>
Validation Schemas
With Zod
import { parseWithZod, getZodConstraint } from '@conform-to/zod'; const [form, fields] = useForm({ constraint: getZodConstraint(schema), onValidate({ formData }) { return parseWithZod(formData, { schema }); }, });
With Yup
import { parseWithYup, getYupConstraint } from '@conform-to/yup'; const [form, fields] = useForm({ constraint: getYupConstraint(schema), onValidate({ formData }) { return parseWithYup(formData, { schema }); }, });
With Valibot
import { parseWithValibot, getValibotConstraint } from '@conform-to/valibot'; const [form, fields] = useForm({ constraint: getValibotConstraint(schema), onValidate({ formData }) { return parseWithValibot(formData, { schema }); }, });
Error Handling
Field Errors
{fields.email.errors?.map((error, i) => ( <div key={i} className="error">{error}</div> ))}
Form Errors
{form.errors?.map((error, i) => ( <div key={i} className="form-error">{error}</div> ))}
Custom Server Errors
// In action return submission.reply({ fieldErrors: { email: ['Email already registered'], }, formErrors: ['Something went wrong'], });
See references/api.md for complete API reference.