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", React Hook Form, and a Standard Schema library. This example uses Zod.

npm install form-please react-hook-form zod

React 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.

Profile

Edit a personal or company profile.

Choose a PNG file. The File stays in the React Hook Form input.

Contacts

Add or reorder contacts. React Hook Form updates the array by index.

Submission resultSubmit the form to see the result.

Values

{
  "name": "Ada Lovelace",
  "accountType": "personal",
  "country": "GB",
  "newsletter": true,
  "contacts": [
    {
      "email": "ada@example.com",
      "label": "primary"
    }
  ]
}

State

Dirty
false
Touched
false
Validation
idle
Submits
0
Rows
1

Visible issues

No visible issues

Browser FormData (diagnostic)

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">{}</>
		</>
	)
}