@liam-michel/validated-form
v0.2.1
Published
A type-safe form library built on React Hook Form and Zod
Maintainers
Readme
@liam-michel/validated-form
A type-safe, schema-driven form library for React. Define a Zod schema and get validated, fully-typed form fields out of the box — built on React Hook Form and Radix UI.
Installation
npm install @liam-michel/validated-formPeer dependencies
You'll also need these in your project:
npm install react react-dom react-hook-form zod @hookform/resolvers \
@radix-ui/react-checkbox @radix-ui/react-select @radix-ui/react-slider \
@radix-ui/react-popover cmdk react-day-picker date-fns lucide-react \
clsx tailwind-mergeThe library uses Tailwind CSS class names. Make sure your project has Tailwind configured.
Quick start
import {
ValidatedForm,
TextField,
NumberField,
CheckboxField,
} from '@liam-michel/validated-form';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(1, 'Name is required'),
age: z.number().min(1, 'Age is required'),
agree: z.boolean().refine((v) => v, 'You must agree'),
});
function MyForm() {
return (
<ValidatedForm
schema={schema}
defaultValues={{ name: '', age: 0, agree: false }}
onSubmit={async (data) => {
// data is fully typed as { name: string; age: number; agree: boolean }
console.log(data);
}}
>
<TextField<typeof schema>
name="name"
label="Name"
placeholder="Enter your name"
/>
<NumberField<typeof schema> name="age" label="Age" />
<CheckboxField<typeof schema> name="agree" label="I agree to the terms" />
</ValidatedForm>
);
}createForm — schema-bound factory
For a cleaner API without manual generics, use createForm to generate a full set of typed components from a single schema:
import { createForm } from '@liam-michel/validated-form';
import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
bio: z.string(),
role: z.enum(['admin', 'user', 'viewer']),
});
const { Form, TextField, TextAreaField, SelectField } = createForm(schema);
function ProfileForm() {
return (
<Form
onSubmit={async (data) => console.log(data)}
defaultValues={{ email: '', bio: '', role: 'user' }}
>
<TextField name="email" label="Email" />
<TextAreaField
name="bio"
label="Bio"
placeholder="Tell us about yourself"
/>
<SelectField
name="role"
label="Role"
options={[
{ value: 'admin', label: 'Admin' },
{ value: 'user', label: 'User' },
{ value: 'viewer', label: 'Viewer' },
]}
/>
</Form>
);
}useValidatedForm — external form control
Use the useValidatedForm hook when you need access to the form instance outside of the <ValidatedForm> component (e.g. for programmatic control, multi-step forms, or watching values):
import {
ValidatedForm,
TextField,
useValidatedForm,
} from '@liam-michel/validated-form';
import { z } from 'zod';
const schema = z.object({
name: z.string().min(1),
});
function ControlledForm() {
const form = useValidatedForm({
schema,
defaultValues: { name: '' },
});
return (
<ValidatedForm
schema={schema}
form={form}
onSubmit={async (data) => console.log(data)}
>
<TextField<typeof schema> name="name" label="Name" />
</ValidatedForm>
);
}Field components
All fields share these common props:
| Prop | Type | Description |
| ------------- | --------------------- | --------------------------------------------------- |
| name | typed key from schema | The field name (type-safe based on your Zod schema) |
| label | string | Label text displayed above the field |
| description | string? | Helper text displayed below the field |
| placeholder | string? | Placeholder text |
| disabled | boolean? | Disable the field |
| className | string? | Additional CSS class for the wrapper |
| showReset | boolean? | Show a reset button to restore the default value |
Available fields
| Component | Schema type | Extra props |
| ------------------ | --------------------- | -------------------------------------------------------------- |
| TextField | z.string() | type?: HTMLInputTypeAttribute (e.g. "password", "email") |
| TextAreaField | z.string() | — |
| NumberField | z.number() | — |
| CheckboxField | z.boolean() | — |
| SelectField | z.enum(...) | options: { value, label }[] |
| SliderField | z.number() | min?, max?, step? |
| DateField | z.date() | — |
| MultiSelectField | z.array(z.string()) | options: { value, label }[] |
ValidatedForm props
| Prop | Type | Description |
| --------------- | ---------------------------------- | -------------------------------------------------------- |
| schema | ZodType | Zod schema for validation |
| onSubmit | (data: Output) => Promise<void> | Submit handler (receives validated & typed data) |
| defaultValues | DefaultValues? | Initial form values |
| form | UseFormReturn? | External form instance from useValidatedForm |
| submitLabel | string? | Custom label for the submit button (default: "Submit") |
| hideSubmit | boolean? | Hide the default submit button |
| renderSubmit | (state) => ReactNode | Custom submit button renderer |
| renderError | (message) => ReactNode | Custom root error renderer |
| children | ReactNode \| (form) => ReactNode | Fields, or a render function receiving the form instance |
Error handling
When onSubmit throws, errors are handled automatically:
- Field-level errors: Throw
{ data: { fieldErrors: { fieldName: "message" } } }to set errors on specific fields. - Root-level errors: Any other thrown error displays its
messageas a form-level error.
<ValidatedForm
schema={schema}
onSubmit={async (data) => {
const res = await api.createUser(data);
if (!res.ok) {
// Field-level errors
throw { data: { fieldErrors: { email: 'Email already taken' } } };
}
}}
>
{/* ... */}
</ValidatedForm>Type helpers
The library exports type helpers to extract field names by type from a schema:
import type {
StringFieldsOf,
NumberFieldsOf,
BooleanFieldsOf,
DateFieldsOf,
SelectFieldsOf,
} from '@liam-michel/validated-form';These are used internally by the field components and can be useful if you're building custom fields.
License
MIT
