Form
Building forms with TanStack Form, Zod validation, and accessible Cubitt field components.
Overview
The Form components bind TanStack Form state to Cubitt field primitives. The form chrome handles labels, descriptions, errors, context, and submission wiring while the underlying input, select, checkbox, switch, and related components keep ownership of their own surfaces, borders, and interaction states.
Use FormBuilder for most cases
For most use cases, use Form Builder instead of raw form components. It renders forms from typed formDefs with built-in conditional logic, multi-step wizards, and automatic layout.
Usage
import { Button } from "@tilt-legal/cubitt-components/button";
import {
Form,
InputField,
} from "@tilt-legal/cubitt-components/form";
import { revalidateLogic, useForm } from "@tanstack/react-form";
import { z } from "zod";const schema = z.object({
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
});
const form = useForm({
defaultValues: {
email: "",
password: "",
},
validationLogic: revalidateLogic({
mode: "submit",
modeAfterSubmission: "change",
}),
validators: { onDynamic: schema },
onSubmit: async ({ value }) => {
console.log("Form submitted:", value);
},
});return (
<Form form={form} className="max-w-md">
<InputField
name="email"
label="Email"
type="email"
placeholder="m@example.com"
required
/>
<InputField
name="password"
label="Password"
type="password"
placeholder="Password"
required
/>
<Button type="submit">Login</Button>
</Form>
);Form Components
Form
The root form component provides context, handles submit events, and exposes useful state attributes.
<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 |
noValidate | boolean | Disable browser validation. Defaults to true. |
className | string | Layout classes for the form element |
FormField
Low-level wrapper for field content and error messages. Most fields include this automatically.
import { Input } from "@tilt-legal/cubitt-components/input";
import { FormField } from "@tilt-legal/cubitt-components/form";
<FormField fieldName="username" messages={errors}>
<Input id="username" aria-invalid={errors.length > 0} />
</FormField>| Prop | Type | Description |
|---|---|---|
messages | string[] | Error messages |
fieldName | string | Field name for IDs |
size | "xs" | "sm" | "md" | "lg" | Message size |
className | string | Layout classes |
FormFieldSet
Groups related fields with an optional legend.
<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 legend |
messages | string[] | Group-level errors |
fieldName | string | Field name for IDs |
size | "xs" | "sm" | "md" | "lg" | Message size |
className | string | Layout classes |
Context Hook
Use useTanstackFormContext when a nested component needs access to the current form instance.
import { Button } from "@tilt-legal/cubitt-components/button";
import { useTanstackFormContext } from "@tilt-legal/cubitt-components/form";
import { useStore } from "@tanstack/react-form";
function SubmitButton() {
const form = useTanstackFormContext();
const canSubmit = useStore(form.store, (state) => state.canSubmit);
return (
<Button type="submit" disabled={!canSubmit}>
Save
</Button>
);
}Examples
import { Button } from "@tilt-legal/cubitt-components/button";
import { Card, CardContent } from "@tilt-legal/cubitt-components/card";
import {
Form,
InputField,
} from "@tilt-legal/cubitt-components/form";
import { revalidateLogic, useForm } from "@tanstack/react-form";
import { z } from "zod";const schema = z.object({
email: z.email("Invalid email address"),
password: z.string().min(8, "Password must be at least 8 characters"),
});
export function BasicFormExample() {
const form = useForm({
defaultValues: {
email: "",
password: "",
},
validationLogic: revalidateLogic({
mode: "submit",
modeAfterSubmission: "change",
}),
validators: { onDynamic: schema },
onSubmit: async ({ value }) => {
console.log(value);
},
});
return (
<Form className="max-w-sm" form={form} invalidAnimation="shake">
<Card>
<CardContent>
<InputField label="Email" name="email" type="email" />
<InputField label="Password" name="password" type="password" />
<Button type="submit">Login</Button>
</CardContent>
</Card>
</Form>
);
}See all field components on the Fields page.