Get started
Build and submit a working form in three steps. This example uses the native preset, Zod, and the default validation timing.
Install
Install "Form, Please", React Hook Form, and a Standard Schema library. This example uses Zod.
npm install form-please react-hook-form zodReact 18 and React 19 are supported peer dependencies.
1. Define the data contract
Create a Standard Schema for the editable input. This schema keeps the editable
values unchanged. It adds slug only to the successful submit output.
import { nativeFormKit as kit } from "form-please/preset-native"
import { z } from "zod"
const profileSchema = z
.object({
name: z.string().min(2, "Enter at least two characters"),
email: z.string().email("Enter a valid email"),
})
.transform((input) => ({
...input,
slug: input.name.trim().toLowerCase().replaceAll(" ", "-"),
}))The schema input is { name: string; email: string }. The schema output also
contains slug. Generated fields always edit the schema input.
2. Define the generated fields
Connect each schema path to a control from the native preset:
const profileDefinition = kit.defineForm(profileSchema, {
ui: [
{
kind: "field",
path: "name",
control: "text",
label: "Name",
required: true,
},
{
kind: "field",
path: "email",
control: "text",
label: "Email",
props: { type: "email", autoComplete: "email" },
required: true,
},
],
})path must exist in the schema input. The selected control must accept the
value at that path. required: true changes the UI state only. The Zod schema
still owns the validation rule.
3. Bind and render the form
Create the form binding inside a React component. Supply one complete schema
input as defaultValues.
export function ProfileForm() {
const form = kit.useForm(profileDefinition, {
defaultValues: { name: "", email: "" },
onSubmit({ value }) {
// `value` includes the transformed `slug`.
console.log(value)
},
})
return (
<kit.AutoForm form={form}>
<kit.Submit>Save profile</kit.Submit>
</kit.AutoForm>
)
}kit.AutoForm renders the native <form>, an error summary, and the generated
fields. Add kit.Submit as a child to render the submit button.
After a valid submit, value contains the schema output, including slug.
Use the input callback property when you also need the editable input.
All JavaScript package entries are React client modules. In a Next.js App
Router project, put "use client" at the start of the component file.
Copy the complete first form
"use client"
import { as } from "form-please/preset-native"
import { } from "zod"
const =
.({
: .().(2, "Enter at least two characters"),
: .().("Enter a valid email"),
})
.(() => ({
...,
: ..().().(" ", "-"),
}))
const = .(, {
: [
{
: "field",
: "name",
: "text",
: "Name",
: true,
},
{
: "field",
: "email",
: "text",
: "Email",
: { : "email", : "email" },
: true,
},
],
})
export function () {
const = .(, {
: { : "", : "" },
({ }) {
// `value` includes the transformed `slug`.
.()
},
})
return (
<. ={}>
<.>Save profile</.>
</.>
)
}Add the optional layout
Import the optional structural stylesheet once if you want baseline spacing:
import "form-please/layout.css"The native preset is intentionally unstyled. Replace it with your own controls and slots, or use the Material UI preset.
Try the interactive lab
The lab adds conditional fields, an editable contact array, reset, live
React Hook Form state, and a diagnostic snapshot of native browser FormData.
Submission uses the React Hook Form values, not that snapshot. Submit the form
to see the transformed output.
Interactive lab
Edit the generated form. Compare its values and visible issues with a diagnostic browser FormData snapshot. Submission uses React Hook Form values.
Copy the complete interactive lab form
"use client"
import { , type , type } from "form-please"
import { } from "form-please/default-slots"
import { } from "form-please/native-controls"
import { } from "react"
import { } from "zod"
const =
.({
: .().(1, "Name is required"),
: .(["personal", "company"]),
: .().(),
: .().(2, "Choose a country"),
: .(),
:
.<File | undefined>(
() =>
=== ||
(typeof !== "undefined" && instanceof ),
"Choose a browser File",
)
.(),
:
.(
.({
: .().("Use a valid email"),
: .().(),
}),
)
.(1, "Add at least one contact"),
})
.((, ) => {
if (
. === "company" &&
(. ?? "").(). === 0
) {
.({
: "custom",
: "Company name is required",
: ["companyName"],
})
}
})
.(() => ({
...,
: .?.() || ,
: ..,
}))
export type = <typeof >
export type = <typeof >
export const = {
: "Ada Lovelace",
: "personal",
: "GB",
: true,
: [{ : "ada@example.com", : "primary" }],
} satisfies
const = [
{ : "GB", : "United Kingdom" },
{ : "US", : "United States" },
{ : "NL", : "Netherlands" },
]
export const = ({
: (),
: ({
: {
: "Add contact",
: ({ }) => `Move contact ${} down`,
: ({ }) => `Move contact ${} up`,
: ({ }) => `Remove contact ${}`,
},
}),
})
export const = .(, {
: [
{
: "section",
: "account",
: "Profile",
: "Edit a personal or company profile.",
: ({ }) => {
if ( === "company") {
return "rounded-2xl border border-amber-300 bg-amber-50 p-5 shadow-sm transition-colors dark:border-amber-700 dark:bg-amber-950/30"
}
return "rounded-2xl border border-emerald-300 bg-emerald-50 p-5 shadow-sm transition-colors dark:border-emerald-700 dark:bg-emerald-950/30"
},
: 2,
: [
{
: "field",
: "name",
: "text",
: "Name",
: true,
: {
: "Enter your name",
: "name",
},
},
{
: "field",
: "accountType",
: "select",
: "Account type",
: true,
: [
{ : "personal", : "Personal" },
{ : "company", : "Company" },
],
},
{
: "field",
: "companyName",
: "text",
: "Company name",
: ({ }) => === "company",
: ({ }) => === "company",
: {
: "Compiler Labs",
: "organization",
},
},
{
: "field",
: "country",
: "select",
: "Country",
: true,
: ,
},
{
: "field",
: "newsletter",
: "checkbox",
: "Receive product news",
},
{
: "field",
: "avatar",
: "file",
: "Avatar",
:
"Choose a PNG file. The File stays in the React Hook Form input.",
: {
: "image/png",
},
},
],
},
{
: "array",
: "contacts",
: "Contacts",
:
"Add or reorder contacts. React Hook Form updates the array by index.",
: {
: "",
: ,
},
: [
{
: "field",
: "email",
: "text",
: "Email",
: true,
: {
: "email",
: "ada@example.com",
: "email",
},
},
{
: "field",
: "label",
: "text",
: "Label",
: {
: "primary",
},
},
],
},
],
})
export function () {
const [, ] = <>()
const = .(, {
,
: ({ }) => (),
})
let = "Submit the form to see typed output."
if ( !== ) = .(, null, 2)
return (
<>
<. ={}>
<.>Save profile</.>
</.>
< ="polite">{}</>
</>
)
}