Form
Validate, create and edit a group of fields with one Zod schema.
Example
Profile
Installation
bunx --bun shadcn@latest add @fieldkit/formNeeds the @fieldkit registry. Add it once
Usage
Terminal
import { Form } from "@/components/form/form"import { FormEmail } from "@/components/form/form-email"import { FormActions, FormLayout } from "@/components/form/form-layout"import { FormRow } from "@/components/form/form-row"import { FormSubmit } from "@/components/form/form-submit"import { FormText } from "@/components/form/form-text"import { FieldGroup } from "@/components/ui/field"
<FormLayout title="Profile"> <Form schema={schema} defaultValues={defaults} onSubmit={(values) => save(values)}> <FieldGroup> <FormRow> <FormText name="firstName" /> <FormText name="lastName" /> </FormRow> <FormEmail name="email" /> <FormActions> <FormSubmit>Save profile</FormSubmit> </FormActions> </FieldGroup> </Form></FormLayout>
// Edit an existing record<Form key={member.id} mode="edit" schema={memberSchema} defaultValues={toFormValues(member)} onSubmit={(values, { changed }) => updateMember(member.id, changed)}> <FieldGroup> <FormMessage /> ... <FormActions> <FormReset /> <FormSubmit>Save changes</FormSubmit> </FormActions> <FormLeaveGuard /> </FieldGroup></Form>
// Show server errors on fieldsonSubmit={async (values) => { const result = await createMember(values) if (result.emailTaken) { throw new FormSubmitError({ message: "Fix the highlighted field and try again.", fields: { email: "This email already has an account" }, }) }}}API reference
Form
PropDescription
schema (required)z.ZodTypeZod v4 schema. Inputs read their rules and the required asterisk from it.defaultValues (required)z.input<TSchema>Empty values to create, the record's values to edit.onSubmit(values, { changed, payload, form }) => void | Promise<void>Gets the parsed output, the keys that changed, and payload to send. Throw FormSubmitError to show server errors. Optional when storage has an adapter with submit.storage{ formId, folder?, maxImageMb?, imageTypes?, maxFileMb?, fileTypes?, adapter? }Where this form keeps rich text images and files, uploaded files and, without onSubmit, its submissions. adapter falls back to FormStorageProvider.recordIdstringSent to storage.adapter.submit when editing a record.mode"create" | "edit" = "create"In edit mode FormSubmit waits for a change.validateOn"onChange" | "onBlur" | "onSubmit" = formConfig.validateOnWhen fields validate before submit.editMethod"put" | "patch" = formConfig.editMethodIn edit mode, payload holds every value for put, only changed values for patch.messagesPartial<SubmitMessages> = formConfig.submit.messagesToast titles for this form. success: false hides the success toast.FormSubmitError
PropDescription
messagestringShown by FormMessage above the fields and in the error toast.fieldsRecord<string, string>Errors shown on each named field until it changes.FormField and FormFieldFrame
PropDescription
name (required)stringKey in the schema.requiredbooleanOverrides the asterisk read from the schema.validatorsFieldValidatorsExtra field checks, such as onBlurAsync.Notes
- FormSubmit shows a spinner until onSubmit resolves. Return its promise so the form waits for the save.
- After a successful submit the form resets to what was saved, so edit mode shows no changes again.
- Change key to load a different record into the same form.
- Any other thrown error shows its message in FormMessage and an error toast.
- Mount Toaster from @/components/ui/toast once in the root layout. Toasts do not render without it.
- The asterisk shows when the schema rejects an empty value.