# Form

A form that collects, validates and submits its fields.

Source: [form.tsx](https://github.com/jakejarvis/jarvis-ui/blob/main/src/components/ui/form.tsx)

```tsx
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
  Field,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldItem,
  FieldLabel,
  FieldLegend,
  FieldSet,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { toast } from "@/components/ui/toast";

const regions = { iad: "Washington, D.C.", sfo: "San Francisco", fra: "Frankfurt" };

export default function FormBasic() {
  return (
    <Form
      className="max-w-sm"
      onFormSubmit={(values) =>
        toast.add({ title: "Project created", description: `${values.name} is ready.` })
      }
    >
      <FieldGroup>
        <Field name="name">
          <FieldLabel>Project name</FieldLabel>
          <Input required minLength={3} placeholder="Acme Dashboard" />
          <FieldError match="valueMissing">Give the project a name.</FieldError>
          <FieldError match="tooShort">Use at least 3 characters.</FieldError>
        </Field>
        <Field name="region">
          <FieldLabel>Region</FieldLabel>
          <Select items={regions} defaultValue="iad">
            <SelectTrigger className="w-full">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              {Object.entries(regions).map(([value, label]) => (
                <SelectItem key={value} value={value}>
                  {label}
                </SelectItem>
              ))}
            </SelectContent>
          </Select>
          <FieldDescription>Where its data is stored.</FieldDescription>
        </Field>
        <Field name="visibility">
          <FieldSet>
            <FieldLegend variant="label">Visibility</FieldLegend>
            <RadioGroup defaultValue="team">
              <FieldItem>
                <RadioGroupItem value="team" />
                <FieldLabel>Everyone on the team</FieldLabel>
              </FieldItem>
              <FieldItem>
                <RadioGroupItem value="private" />
                <FieldLabel>Only people I invite</FieldLabel>
              </FieldItem>
            </RadioGroup>
          </FieldSet>
        </Field>
        <Field name="starter" orientation="horizontal">
          <Checkbox defaultChecked />
          <FieldLabel>Add sample data</FieldLabel>
        </Field>
      </FieldGroup>
      <Button type="submit" variant="primary" className="self-start">
        Create project
      </Button>
    </Form>
  );
}
```

## Installation

Add the registry, once per project:

```bash
npx shadcn@latest registry add @jarvis=https://ui.jarv.is/r/{name}.json
```

Then add the component:

```bash
npx shadcn@latest add @jarvis/form
```

## Usage

```tsx
import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";

export function NewsletterForm() {
  return (
    <Form onFormSubmit={(values) => console.log(values.email)}>
      <Field name="email">
        <FieldLabel>Email</FieldLabel>
        <Input type="email" required />
        <FieldError />
      </Field>
      <Button type="submit" variant="primary">
        Subscribe
      </Button>
    </Form>
  );
}
```

`Form` is built from [Field](https://ui.jarv.is/components/field.md): give each `Field` a `name`, and the form submits the
value of the control inside it under that name. Every control built on Base UI works this way, from
an `Input` to a `Select`, `Slider` or `CheckboxGroup`, and so do `Textarea` and `NativeSelect`. A
`DatePicker` keeps its value in state, so send it yourself. A `FileUpload` takes a `name` of its
own, and its files reach the browser's `FormData` but not `onFormSubmit`.

`onFormSubmit` receives the values as an object once every field is valid, and prevents the page
from reloading. `onSubmit` and `action` work as they do on a native `<form>`, with the browser's
`FormData`.

## Examples

### Every control

Each control submits under its field's `name`. A checkbox or radio group is one field, with a
`FieldItem` around each option.

```tsx
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import { CheckboxGroup } from "@/components/ui/checkbox-group";
import {
  Field,
  FieldContent,
  FieldDescription,
  FieldError,
  FieldGroup,
  FieldItem,
  FieldLabel,
  FieldLegend,
  FieldSet,
} from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import {
  NumberField,
  NumberFieldDecrement,
  NumberFieldGroup,
  NumberFieldIncrement,
  NumberFieldInput,
} from "@/components/ui/number-field";
import { Slider } from "@/components/ui/slider";
import { Switch } from "@/components/ui/switch";
import { Textarea } from "@/components/ui/textarea";
import { toast } from "@/components/ui/toast";

const channels = [
  { value: "email", label: "Email" },
  { value: "slack", label: "Slack" },
  { value: "sms", label: "Text messages" },
];

export default function FormControls() {
  return (
    <Form
      className="max-w-sm"
      onFormSubmit={(values) =>
        toast.add({ title: "Settings saved", description: JSON.stringify(values) })
      }
    >
      <FieldGroup>
        <Field name="seats">
          <FieldLabel>Seats</FieldLabel>
          <NumberField defaultValue={5} min={1} max={50} className="w-28">
            <NumberFieldGroup>
              <NumberFieldInput />
              <NumberFieldDecrement />
              <NumberFieldIncrement />
            </NumberFieldGroup>
          </NumberField>
        </Field>
        <Field name="budget">
          <FieldLabel>Monthly budget</FieldLabel>
          <Slider defaultValue={40} step={10} />
          <FieldDescription>We alert you as spending nears it.</FieldDescription>
        </Field>
        <Field
          name="channels"
          validate={(value) =>
            Array.isArray(value) && value.length ? null : "Choose at least one."
          }
        >
          <FieldSet>
            <FieldLegend variant="label">Alert me by</FieldLegend>
            <CheckboxGroup defaultValue={["email"]}>
              {channels.map((channel) => (
                <FieldItem key={channel.value}>
                  <Checkbox value={channel.value} />
                  <FieldLabel>{channel.label}</FieldLabel>
                </FieldItem>
              ))}
            </CheckboxGroup>
          </FieldSet>
          <FieldError />
        </Field>
        <Field name="notes">
          <FieldLabel>Notes</FieldLabel>
          <Textarea placeholder="Anything we should know" />
        </Field>
        <Field name="autoRenew" orientation="horizontal">
          <FieldContent>
            <FieldLabel>Renew automatically</FieldLabel>
            <FieldDescription>On the first of each month.</FieldDescription>
          </FieldContent>
          <Switch defaultChecked />
        </Field>
      </FieldGroup>
      <Button type="submit" variant="primary" className="self-start">
        Save settings
      </Button>
    </Form>
  );
}
```

### Validation

Fields check the native constraints on their control, such as `required`, `minLength` and
`pattern`. `validate` adds a check of your own: return a message when the value is invalid, or
`null` when it's fine.

A `FieldError` with no children shows the browser's message for whichever check failed. Pass
`match` with a [validity state](https://developer.mozilla.org/en-US/docs/Web/API/ValidityState),
such as `valueMissing`, to show your own message for one check, or `customError` for the messages
`validate` returns.

Fields validate when the form is submitted, then again on every change. `validationMode` on the
form or a field changes that to `onBlur` or `onChange`.

```tsx
import { Button } from "@/components/ui/button";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";

const reserved = new Set(["admin", "root", "support"]);

export default function FormValidation() {
  return (
    <Form className="max-w-sm" validationMode="onChange">
      <Field
        name="username"
        validate={(value) =>
          reserved.has(String(value).toLowerCase()) ? "That name is reserved." : null
        }
      >
        <FieldLabel>Username</FieldLabel>
        <Input required pattern="[a-z0-9\-]+" placeholder="ada-park" />
        <FieldDescription>Lowercase letters, numbers and dashes.</FieldDescription>
        <FieldError match="valueMissing">Choose a username.</FieldError>
        <FieldError match="patternMismatch">
          Use only lowercase letters, numbers and dashes.
        </FieldError>
        <FieldError match="customError" />
      </Field>
      <Button type="submit" variant="primary" className="self-start">
        Save
      </Button>
    </Form>
  );
}
```

### Server errors

Pass the errors a server returns to `errors`, keyed by field name. Each shows in its field's
`FieldError` until the value changes.

```tsx
import * as React from "react";

import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { toast } from "@/components/ui/toast";

async function redeem(code: string): Promise<Record<string, string>> {
  await new Promise((resolve) => setTimeout(resolve, 800));
  return code.toUpperCase() === "WELCOME" ? {} : { code: "That code has expired." };
}

export default function FormServerErrors() {
  const [errors, setErrors] = React.useState<Record<string, string>>({});
  const [pending, setPending] = React.useState(false);

  return (
    <Form
      className="max-w-sm"
      errors={errors}
      onFormSubmit={async (values: { code: string }) => {
        setPending(true);
        const result = await redeem(values.code);
        setErrors(result);
        setPending(false);
        if (!result.code) {
          toast.add({ title: "Code redeemed", type: "success" });
        }
      }}
    >
      <Field name="code">
        <FieldLabel>Promo code</FieldLabel>
        <Input required defaultValue="SPRING24" />
        <FieldError />
      </Field>
      <Button type="submit" variant="primary" disabled={pending} className="self-start">
        {pending ? "Checking…" : "Redeem"}
      </Button>
    </Form>
  );
}
```

### With an action

`action` takes a function from `useActionState`, so the form works with React's Server Functions.

```tsx
import * as React from "react";

import { Button } from "@/components/ui/button";
import { Field, FieldError, FieldLabel } from "@/components/ui/field";
import { Form } from "@/components/ui/form";
import { Input } from "@/components/ui/input";

type State = { email: string; errors?: Record<string, string>; subscribed?: string };

async function subscribe(_state: State, formData: FormData): Promise<State> {
  await new Promise((resolve) => setTimeout(resolve, 800));
  const email = String(formData.get("email"));
  if (email.endsWith("@example.com")) {
    return { email, errors: { email: "We can’t send to example.com addresses." } };
  }
  return { email: "", subscribed: email };
}

export default function FormActionState() {
  const [state, action, pending] = React.useActionState(subscribe, { email: "ada@example.com" });

  return (
    <Form className="max-w-sm" action={action} errors={state.errors}>
      <Field name="email">
        <FieldLabel>Email</FieldLabel>
        <Input type="email" required defaultValue={state.email} />
        <FieldError />
      </Field>
      <div className="flex items-center gap-3">
        <Button type="submit" variant="primary" disabled={pending}>
          Subscribe
        </Button>
        {state.subscribed && (
          <p role="status" className="text-sm text-muted-foreground">
            Subscribed {state.subscribed}.
          </p>
        )}
      </div>
    </Form>
  );
}
```

## Props

`Form` renders a `<form>` that stacks its children. Errors and invalid fields belong to each
[Field](https://ui.jarv.is/components/field.md).

| Part | Extends |
| --- | --- |
| `Form` | [`Form`](https://base-ui.com/react/components/form#api-reference) |

The tables leave out `className`, `style` and `render`. Parts built on Base UI take them as its [styling](https://base-ui.com/react/handbook/styling) and [composition](https://base-ui.com/react/handbook/composition) guides describe.

### Form

| Prop              | Type                                                                                     | Description                                                                                                                                                                                                                                                                                                                                                                          |
| ----------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `validationMode?` | `"onBlur" \| "onChange" \| "onSubmit"`                                                   | Determines when the form should be validated. The `validationMode` prop on `<Field.Root>` takes precedence over this. - `onSubmit` (default): validates the field when the form is submitted, afterwards fields will re-validate on change. - `onBlur`: validates a field when it loses focus. - `onChange`: validates the field on every change to its value. Default: `'onSubmit'` |
| `errors?`         | `Errors`                                                                                 | Validation errors returned externally, typically after submission by a server or a form action. This should be an object where keys correspond to the `name` attribute on `<Field.Root>`, and values correspond to error(s) related to that field.                                                                                                                                   |
| `onFormSubmit?`   | `((formValues: Record<string, unknown>, eventDetails: Form.SubmitEventDetails) => void)` | Event handler called when the form is submitted. `preventDefault()` is called on the native submit event when used.                                                                                                                                                                                                                                                                  |
| `actionsRef?`     | `React.RefObject<FormActions \| null>`                                                   | A ref to imperative actions. - `validate`: Validates all fields when called. Optionally pass a field name to validate a single field.                                                                                                                                                                                                                                                |
