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>
PropTypeDescription
formAnyReactFormApiTanStack Form instance from useForm()
invalidAnimation"shake"Optional shake animation when form is invalid
invalidOverridebooleanOverride the computed invalid state
initialValidatebooleanRun validation on initial mount
noValidatebooleanDisable browser validation. Defaults to true.
classNamestringLayout 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>
PropTypeDescription
messagesstring[]Error messages
fieldNamestringField name for IDs
size"xs" | "sm" | "md" | "lg"Message size
classNamestringLayout 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>
PropTypeDescription
titleReact.ReactNodeFieldset legend
messagesstring[]Group-level errors
fieldNamestringField name for IDs
size"xs" | "sm" | "md" | "lg"Message size
classNamestringLayout 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

See all field components on the Fields page.

On this page