FormKit and FieldKit — Zod schemas with React Hook Form.
The form workspace package provides schema-driven controls for product forms and inline field surfaces. FormKit owns the submit lifecycle; FieldKit owns field state without adding a submit button. Both use Zod, React Hook Form, and the same field registry.
When to use it
| Pattern | Use |
|---|---|
| FormKit | Create/edit screens with validation, server errors, and submit/reset actions |
| FieldKit | Toolbar filters, drawer filters, or any UI where you own values / onValuesChange |
Low-level Field* exports | Custom layouts outside FormKit/FieldKit (advanced) |
Reach for FormKit when the page has a single schema and an onSubmit. Reach for FieldKit when the parent already stores filter state (often next to a DataTable).
Installation
Registry CLI
Requires a one-time components.json registry entry for @feature-kit/form.
pnpm dlx shadcn@latest add https://feature-kits.vercel.app/r/form.jsonNamespaced: npx shadcn@latest add @feature-kit/form
Manual installation
Monorepo consumers can depend on workspace:* packages instead of the registry CLI.
pnpm add form layout uiDependencies
// apps/your-app/package.json
"dependencies": {
"form": "workspace:*"
"layout": "workspace:*"
"ui": "workspace:*"
}Transpile (Next.js)
// next.config.ts
transpilePackages: ["form", "layout", "ui"]Import
import { FormKit } from "@/lib/feature-kit/form"
import { FieldKit } from "@/lib/feature-kit/form/field-kit"
import { FormSection } from "@/lib/feature-kit/layout"Apps need next-intl messages for field labels if you use translated error keys.
Choose an API
| Need | Use |
|---|---|
| Create or edit data with submit, reset, pending, and server-error handling | FormKit |
| Controlled filters or inline fields next to a table | FieldKit |
| A custom layout around one control | Field* exports from form or form/field |
| Searchable option controls | SearchableSelect or SearchableMultiSelect |
FormKit and FieldKit are client components. Keep the schema and default values in the client component that renders them.
Anatomy
FormKit is a compound component:
FormKit— creates the form context fromschema+defaultValuesFormKit.Field— dispatches to the correct control fromtypeFormKit.Actions— footer row for buttonsFormKit.Submit/FormKit.Reset— wired to form state (isPending, dirty, etc.)
FieldKit mirrors the same Field API with mode="controlled" (default) or mode="uncontrolled".
Usage
Basic profile form
Field types
FormKit.Field accepts a discriminated type. Supported values in this repo:
type | Control |
|---|---|
text | Text input (inputType: text, email, tel, password) |
textarea | Multiline text |
select / multi-select | Searchable lists |
checkbox / switch | Boolean |
checkbox-choice-card / switch-choice-card / radio-choice-card | Card-style choices |
radio-group | Radio list |
date / date-range / month / quarter / time | Date & time pickers |
See Field types example for a live matrix.
Validation
Pass a Zod schema to FormKit. The resolver validates before onSubmit runs and focuses the first invalid field. Use Zod for interaction feedback, but keep authoritative business validation on the server.
const schema = z.object({
email: z.string().email("Enter a valid email"),
terms: z.boolean().refine(Boolean, "Accept the terms"),
})The validation example demonstrates string, numeric-string, regex, range, and boolean rules.
Filter row (FieldKit)
Use FieldKit when there is no form submit—only value changes:
import { FieldKit } from "form/field-kit"
import { z } from "zod"
const filterSchema = z.object({
q: z.string(),
status: z.enum(["all", "active", "archived"]),
})
<FieldKit
schema={filterSchema}
values={filters}
onValuesChange={setFilters}>
<FieldKit.Field type="text" name="q" label="Search" />
<FieldKit.Field type="select" name="status" label="Status" options={statusOptions} />
</FieldKit>Recipes
Server validation errors
Map API failures with mapServerError or call applyServerErrors from the form package after a failed mutation. FormKit calls the mapper when onSubmit throws, applies field/form errors, and focuses the first invalid field. The mapper returns the public MapServerErrorResult shape; the package also exports mapUnknownServerError, resolveFieldErrorMessage, and focusFirstError.
Do not call an undocumented form method to set errors. Throw the mutation error from onSubmit and provide mapServerError, or use applyServerErrors(form.setError, mapped) in a custom React Hook Form integration.
Multi-section pages
Set scrollBody={false} on FormKit when sections live in a parent scroll container (long settings pages). Wrap each group in FormSection from layout.
Loading and pending states
Use isPending for an external mutation state. FormKit.Submit also disables itself while React Hook Form is submitting and displays pendingLabel. FormKit.Reset is disabled while isPending. For initial data loading, render the appropriate Field*Skeleton export before mounting the editable form; skeletons are separate controls, not a FormKit prop.
FieldKit
FieldKit is useful when the host owns the values:
<FieldKit
schema={filterSchema}
mode="controlled"
values={filters}
onValuesChange={setFilters}>
<FieldKit.Field type="text" name="q" label="Search" />
</FieldKit>Controlled mode uses values and onValuesChange. Uncontrolled mode uses defaultValues; mode defaults to "uncontrolled" in the implementation. FieldKit has no submit, reset, or success lifecycle. See the FieldKit example for a controlled filter surface.
API reference
FormKit
| Prop | Type | Default | Description |
|---|---|---|---|
| schema | ZodTypeAny | — | Zod schema used for resolver validation and output typing. |
| defaultValues | DefaultValues<z.input<TSchema>> | — | Initial values; must satisfy the schema input shape. |
| onSubmit | (values: z.output<TSchema>) => void | Promise<void> | — | Called with parsed output when client validation passes. |
| isPending | boolean | false | Disables submit and shows pending UI on Submit. |
| resetOnSuccess | boolean | "keepDirtyValues" | DefaultValues | — | Reset strategy after a successful submit. |
| mapServerError | (error: unknown) => MapServerErrorResult | — | Maps thrown errors to field or form messages. |
| translateError | (message: string) => string | — | Localizes Zod / server message keys. |
| scrollBody | boolean | true | When false, omit internal scroll body for nested page layouts. |
FieldKit
| Prop | Type | Default | Description |
|---|---|---|---|
| schema | ZodTypeAny | — | Shape of filter or inline field values. |
| mode | "controlled" | "uncontrolled" | "uncontrolled" | Controlled uses `values` + `onValuesChange`; uncontrolled uses `defaultValues`. |
| values | z.input<TSchema> | — | Required in controlled mode. |
| onValuesChange | (values) => void | — | Called when any field changes in controlled mode. |
Advanced usage
Reset after success
Set resetOnSuccess to true, "keepDirtyValues", or an object of default values. true restores the original defaultValues; "keepDirtyValues" preserves edits that were made while the submission completed.
Multi-section forms
Use FormSection from layout to group fields. Set scrollBody={false} when the surrounding page owns scrolling. FormKit.Actions places submit controls in the form action area.
Low-level field controls
The package exports FieldText, FieldSelect, FieldDate, FieldDateRange, FieldMonth, FieldQuarter, FieldTime, boolean/card/radio variants, skeletons, fieldRegistry, and FormFieldView. Use these when composing a custom field layout; use FormKit.Field or FieldKit.Field for normal schema-driven forms.
API and exports
The package export map supports:
| Import | Use |
|---|---|
form | FormKit, FieldKit, field controls, comboboxes, utilities, and public types |
form/field-kit | FieldKit and its public types |
form/form-kit | FormKit and its public types |
form/field | field controls and field types |
The root package also re-exports Controller, useFormContext, useFormState, and useWatch from React Hook Form. Internal source paths under feature/form/src are not package imports.
FormKit.Submit / FormKit.Reset
| Prop | Type | Default | Description |
|---|---|---|---|
| label | ReactNode | — | Button label. |
| pendingLabel | ReactNode | — | Shown on Submit while pending. |
| disabled | boolean | — | Force disabled state. |
| className | string | — | Optional class on the button. |
Gotchas
Client components
FormKit and FieldKit are client components. Import them from client files or wrap in a "use client" module.
Numbers
Avoid type="number" inputs. Use text with inputMode="numeric" and Zod coercion if you need numeric fields—plain number inputs drop leading zeros.
i18n
Table and form kits expect next-intl for pagination and some labels. The playground app loads messages/en.json in apps/web/src/app/providers.tsx.
Examples
Package exports: form, form/field-kit, form/field (low-level field components).