

<Preview name="BasicFormExample" />

## Overview [#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.

<Callout type="warn" title="Use FormBuilder for most cases">
  For most use cases, use [Form Builder](/composites/form-builder) instead of raw form primitives. It renders forms from typed `formDefs` with built-in conditional logic, multi-step wizards, and automatic layout.
</Callout>

## Usage [#usage]

```tsx
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";
```

```tsx
// 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-components]

### Form [#form]

The root form component that provides context and handles submission.

```tsx
<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 [#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 [#formfield]

Container for form fields with error message display. Usually you won't use this directly as field components include it automatically.

```tsx
<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     |

<Callout type="info">
  See all field components (e.g., `InputField`, `SelectField`, `TagInputField`)
  on the [Fields page](./fields).
</Callout>

### FormFieldSet [#formfieldset]

Groups related fields with an optional title.

```tsx
<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 [#context-hook]

Access the form instance from any child component:

```tsx
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 [#examples]

<Tabs items="['Preview', 'Code']">
  <Tab value="Preview">
    <Preview name="BasicFormExample" />
  </Tab>

  <Tab value="Code">
    ```tsx
    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>
      );
    }
    ```
  </Tab>
</Tabs>
