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, RHF, and a Standard Schema library. This example uses Zod.
React 18 and React 19 are supported peer dependencies.
Define the data contract
Create a Standard Schema for the schema input. The schema output can contain more values than the schema input.
import type { FormOutput } from "form-please"
import { nativeFormKit as kit } from "form-please/preset-native"
import { useState } from "react"
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 }. Generated fields always
edit the schema input.
Define the generated fields
Connect each schema path to a control from the native preset:
const profileDefinition = kit.defineForm(profileSchema, (ui) => [
ui.field("name", {
control: "text",
label: "Name",
required: true,
}),
ui.field("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. See
required is UI-only.
Bind and render the form
Create the form binding inside a React component. Supply one complete schema
input as defaultValues.
export function ProfileForm() {
const [saved, setSaved] = useState<FormOutput<typeof profileSchema>>()
const form = kit.useForm(profileDefinition, {
defaultValues: { name: "", email: "" },
// `value` includes the transformed `slug`.
onSubmit: ({ value }) => setSaved(value),
})
let output = "Submit the form to see the schema output."
if (saved !== undefined) output = JSON.stringify(saved, null, 2)
return (
<>
<kit.AutoForm form={form}>
<kit.Submit>Save profile</kit.Submit>
</kit.AutoForm>
<pre>
<output aria-live="polite">{output}</output>
</pre>
</>
)
}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.
The onSubmit callback keeps value in React state, and the <pre> element
shows it. Use the input callback property when you also need the schema
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.
See the result
This is the form from step 3. Enter a name and an email, and then click
Save profile. The output shows the schema output with slug. Submit empty
fields to see the validation errors.
Live result
Copy the complete first form
"use client"
import type { } from "form-please"
import { as } from "form-please/preset-native"
import { } from "react"
import { } from "zod"
const =
.({
: .().(2, "Enter at least two characters"),
: .().("Enter a valid email"),
})
.(() => ({
...,
: ..().().(" ", "-"),
}))
const = .(, () => [
.("name", {
: "text",
: "Name",
: true,
}),
.("email", {
: "text",
: "Email",
: { : "email", : "email" },
: true,
}),
])
export function () {
const [, ] = <<typeof >>()
const = .(, {
: { : "", : "" },
// `value` includes the transformed `slug`.
: ({ }) => (),
})
let = "Submit the form to see the schema output."
if ( !== ) = .(, null, 2)
return (
<>
<. ={}>
<.>Save profile</.>
</.>
<>
< ="polite">{}</>
</>
</>
)
}Put fields in columns
Import the optional layout stylesheet once in your application entry point:
import "form-please/layout.css"Then put the fields in a section and set columns:
const profileDefinition = kit.defineForm(profileSchema, (ui) => [
ui.section("profile", {
title: "Profile",
columns: 2,
children: [
ui.field("name", { control: "text", label: "Name", required: true }),
ui.field("email", {
control: "text",
label: "Email",
props: { type: "email", autoComplete: "email" },
required: true,
}),
],
}),
])The section puts name and email in one row. When the section is narrower
than 40rem, it shows one column. Set span: "full" on a field to give it the
full row. See Configure columns and spans.
The stylesheet adds only grid and spacing rules. The native preset has no visual theme. Replace it with your own controls and slots, or use the Material UI preset.
Use React Hook Form next to the fields
The form binding keeps the complete RHF API in form.api. In ProfileForm,
read a value with useWatch after the kit.useForm call:
import { useWatch } from "react-hook-form"
const name = useWatch({ control: form.api.control, name: "name" })Render name as a child of kit.AutoForm. register, Controller, and
useFormState also work there, because kit.AutoForm supplies RHF
FormProvider. Read You keep the React Hook Form
API.
Next steps
- Edit a definition in the playground. See the form, the output, and the TypeScript errors change. Its scenarios show a transform, conditional fields, an array, and a mistyped path.
- Keep a total in sync with
beforeUpdate. Write the rule once in the definition. It can also cancel a change before it commits. - Learn the rules that each form follows.