Skip to content
Form, Please

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

Submit the form to see the schema output.
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