Form
Building forms with TanStack Form, Zod validation, and accessible form components.
Overview
The Form components provide a complete solution for building accessible, type-safe forms with TanStack Form. Features include real-time validation with Zod, async validation, field-level error handling, and a clean API that eliminates boilerplate.
Use FormBuilder for most cases
For most use cases, use Form Builder instead of raw form primitives. It renders forms from typed formDefs with built-in conditional logic, multi-step wizards, and automatic layout.
Usage
import { InputField, Button } from "@tilt-legal/cubitt-components/primitives";
import {
useForm,
revalidateLogic } from "@tanstack/react-form";
import { z } from "zod";
import { Form } from "@tilt-legal/cubitt-components/primitives";// 1. Define your validation schema with Zod
const schema = z.object({
email: z.string().email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
});
// 2. Create the form with TanStack Form
const form = useForm({
defaultValues: {
email: "",
password: "",
},
validationLogic: revalidateLogic({
mode: "submit",
modeAfterSubmission: "change",
}),
validators: { onDynamic: schema },
onSubmit: async ({ value }) => {
console.log("Form submitted:", value);
},
});
// 3. Use the component API (dual-prop: name or field)
return (
<Form form={form} className="space-y-4 max-w-md">
<InputField
name="email"
label="Email"
type="email"
placeholder="m@example.com"
required
/>
<InputField
name="password"
label="Password"
type="password"
placeholder="••••••••"
required
/>
<Button type="submit">Login</Button>
</Form>
);Form Components
Form
The root form component that provides context and handles submission.
<Form form={form} invalidAnimation="shake">
{/* form fields */}
</Form>| Prop | Type | Description |
|---|---|---|
form | AnyReactFormApi | TanStack Form instance from useForm() |
invalidAnimation | "shake" | Optional shake animation when form is invalid |
invalidOverride | boolean | Override the computed invalid state |
initialValidate | boolean | Run validation on initial mount (default: false) |
noValidate | boolean | Disable browser validation (default: true) |
className | string | Additional CSS classes |
Features
- Auto-submit handling: Automatically calls
form.handleSubmit()on form submission - Validation state: Computes invalid state from TanStack Form field and form-level errors
- Context provision: Provides form context to all child field components via
useTanstackFormContext() - Animation support: Built-in shake animation for invalid forms
- State attributes: Adds
data-invalid,data-submitting, etc. for CSS hooks
FormField
Container for form fields with error message display. Usually you won't use this directly as field components include it automatically.
<FormField messages={errors} size="md" fieldName="username">
{/* field content */}
</FormField>| Prop | Type | Description |
|---|---|---|
messages | string[] | Error messages |
fieldName | string | Field name for IDs |
size | "xs" | "sm" | "md" | "lg" | Message size |
className | string | Additional CSS |
See all field components (e.g., InputField, SelectField, TagInputField)
on the Fields page.
FormFieldSet
Groups related fields with an optional title.
<FormFieldSet title="Personal Information" fieldName="personal">
<InputField name="firstName" label="First Name" />
<InputField name="lastName" label="Last Name" />
</FormFieldSet>| Prop | Type | Description |
|---|---|---|
title | React.ReactNode | Fieldset title |
messages | string[] | Group-level errors |
fieldName | string | Field name for IDs |
size | "xs" | "sm" | "default" | "lg" | Message size |
className | string | Additional CSS |
Context Hook
Access the form instance from any child component:
import { useTanstackFormContext } from "@tilt-legal/cubitt-components/primitives";
function CustomField() {
const form = useTanstackFormContext();
return (
<form.Field name="custom">
{(field) => (
<input
value={field.state.value}
onChange={(e) => field.handleChange(e.target.value)}
/>
)}
</form.Field>
);
}Examples
import {
FormField,
FormFieldSet,
InputField,
TextareaField,
RadioGroupField,
RadioGroupItem,
SelectField,
SelectTrigger,
SelectValue,
SelectContent,
SelectItem,
Button,
} from "@tilt-legal/cubitt-components/primitives";
import { useForm, revalidateLogic } from "@tanstack/react-form";
import { Form } from "@tilt-legal/cubitt-components/primitives";
const countryItems = [
{ label: "United States", value: "us" },
{ label: "United Kingdom", value: "uk" },
];
export function FullFormExample() {
const form = useForm({
defaultValues: {
firstName: "",
lastName: "",
bio: "",
plan: "free",
country: "",
notes: "",
},
validationLogic: revalidateLogic({
mode: "submit",
modeAfterSubmission: "change",
}),
});
return (
<Form form={form} className="space-y-6 max-w-md">
<FormFieldSet title="Personal information" fieldName="personal">
<InputField name="firstName" label="First name" />
<InputField name="lastName" label="Last name" />
<TextareaField name="bio" label="Bio" rows={3} />
</FormFieldSet>
<FormFieldSet title="Preferences" fieldName="prefs">
<RadioGroupField name="plan" label="Plan">
<RadioGroupItem value="free" id="plan-free">
Free
</RadioGroupItem>
<RadioGroupItem value="pro" id="plan-pro">
Pro
</RadioGroupItem>
</RadioGroupField>
<SelectField items={countryItems} name="country" label="Country">
<SelectTrigger>
<SelectValue placeholder="Select a country" />
</SelectTrigger>
<SelectContent>
{countryItems.map((item) => (
<SelectItem key={item.value} value={item.value}>
{item.label}
</SelectItem>
))}
</SelectContent>
</SelectField>
</FormFieldSet>
<FormField fieldName="notes">
<label htmlFor="notes" className="text-sm font-medium">
Notes
</label>
<textarea
id="notes"
rows={3}
placeholder="Optional notes"
className="mt-1 block w-full rounded-md border border-input bg-background px-3 py-2 text-sm shadow-xs outline-none focus-visible:ring-ring/30 focus-visible:ring-[3px] focus-visible:border-ring"
/>
</FormField>
<Button type="submit">Save</Button>
</Form>
);
}