forms

Build forms with validation, and show server errors next to the right field.

FormsClientBuilt onReact Hook Form
/play/forms/new-invoice
Leave a field empty to see its error. Submit to get typed values, with the amount as a number.

What it is

@kitr/forms is React Hook Form plus the kitr pieces. useSchemaForm builds a form from a schema, so the type and the rules are one thing. <Form> submits to an action or API route, shows a pending state and puts server errors on the right fields.

Runs in the browser. The server half is actions or API routes.

Install

kitr add forms

First time? Run npm i -g @kitr/kitr once. Inside a project, the global kitr runs your project's own version.

Adds @kitr/forms with react-hook-form, its resolvers and zod. No provider, no config file. Every change.

Quick start

Start with a schema, then build the form from it. Each field is a <FormField> with an input inside:

// app/features/invoices/new-invoice-form.tsx
import { FieldInput, Form, FormField, SubmitButton, useRootError, useSchemaForm } from '@kitr/forms'
import { z } from 'zod'

const newInvoiceSchema = z.object({
  customerEmail: z.email('Enter the customer’s email'),
  amount: z.coerce.number().positive('Amount must be more than zero'),
})
type NewInvoice = z.output<typeof newInvoiceSchema>

export function NewInvoiceForm({ onCreate }: { onCreate: (invoice: NewInvoice) => Promise<unknown> }) {
  const form = useSchemaForm(newInvoiceSchema, { defaultValues: { customerEmail: '', amount: '' } })
  return (
    <Form form={form} onSubmit={onCreate} resetOnSuccess aria-label="New invoice">
      <FormField name="customerEmail" label="Customer email">
        <FieldInput type="email" />
      </FormField>
      <FormField name="amount" label="Amount (USD)">
        <FieldInput inputMode="decimal" />
      </FormField>
      <RootError />
      <SubmitButton pendingText="Creating…">Create invoice</SubmitButton>
    </Form>
  )
}

function RootError() {
  const error = useRootError()
  return error ? <p role="alert">{error}</p> : null
}

Render <NewInvoiceForm onCreate={async (invoice) => console.log('created', invoice)} /> on a page. Tab out of the empty email field and "Enter the customer’s email" appears under it. Fill in [email protected] and 120, press the button, and the console prints:

created { customerEmail: '[email protected]', amount: 120 }

amount arrives as the number 120, because onSubmit receives the schema's output. While onCreate runs the button reads "Creating…". Next in the guide: Build your first form.

Built on

What kitr adds Why
useSchemaForm React Hook Form with a Standard Schema resolver, so zod, valibot or arktype schemas type the form.
<Form> Submits a mutation, shows a pending state, and maps KitrError.fieldErrors onto fields.
FormField, useFieldProps, SubmitButton Headless field wiring with aria-* attributes and data-kitr attributes for styling, and a button that follows the pending state.
kitr make:form A generator for a form file with its schema.

kitr adds useSchemaForm, <Form>, the field components with accessibility attributes and the server error mapping.

Problems it solves

Further reading