# Form

Submit related fields and display validation feedback.

Sign-in form

[Complete TSX file](/examples/form/default.tsx)

```tsx
"use client";

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

export default function Example() {
  return (
    <Form
      className="grid w-full max-w-sm gap-4"
      onSubmit={(event) => event.preventDefault()}
    >
      <Field name="email">
        <FieldLabel>Email</FieldLabel>
        <Input
          type="email"
          autoComplete="email"
          placeholder="you@example.com"
          required
        />
        <FieldError>Enter a valid email address.</FieldError>
      </Field>
      <Field name="password">
        <FieldLabel>Password</FieldLabel>
        <Input type="password" autoComplete="current-password" required />
        <FieldError>Enter your password.</FieldError>
      </Field>
      <Button type="submit">Sign in</Button>
    </Form>
  );
}
```

## Installation

Complete [one-time setup](/docs/installation/) first.

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/form
```

With npm: `npx shadcn@4.18.0 add @siteplane/form`. [Registry JSON](/registry/form.json) lists source files and dependencies.

For this page’s examples, also install:

```bash
pnpm dlx shadcn@4.18.0 add @siteplane/button @siteplane/fieldset @siteplane/input @siteplane/select
```

## Usage

```tsx
import { Form } from "@/siteplane/components/form";

<Form onSubmit={(event) => event.preventDefault()}>...</Form>;
```

## Composition

```text
Form
├── Field
│   ├── FieldLabel
│   ├── Input
│   └── FieldError
└── Button type="submit"
```

## Server validation

Pass `errors` keyed by Field name to show the errors your server returns. FieldError displays them, focus moves to the first field with an error, and a field's error clears as soon as its value changes. In this example a simulated request rejects the handle `admin`, and the submit button shows `loading` while the request runs.

Server validation

[Complete TSX file](/examples/form/server-validation.tsx)

```tsx
"use client";

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

// Stands in for your server: it checks the handle and returns errors keyed by
// field name.
async function saveHandle(handle: string): Promise<Record<string, string>> {
  await new Promise((resolve) => window.setTimeout(resolve, 1000));
  return handle === "admin" ? { handle: "This handle is reserved." } : {};
}

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

  return (
    <Form
      className="grid w-full max-w-sm gap-3"
      errors={errors}
      onSubmit={async (event) => {
        event.preventDefault();
        const handle = String(new FormData(event.currentTarget).get("handle"));
        setPending(true);
        setErrors(await saveHandle(handle));
        setPending(false);
      }}
    >
      <Field name="handle">
        <FieldLabel>Public handle</FieldLabel>
        <Input required defaultValue="admin" />
        <FieldDescription>
          Use a unique handle for your profile.
        </FieldDescription>
        <FieldError />
      </Field>
      <Button loading={pending} type="submit">
        Save
      </Button>
    </Form>
  );
}
```

## Controlled reset

Native reset does not reset React state. Restore controlled values in `onReset`.

The validation resets with the form. A reset button or `form.reset()` returns every Field to how it was before the first submit: an error collapses, the field is no longer marked touched or dirty, and the `errors` you passed to Form clear until you pass new ones. The form validates again on its next submit.

Controlled reset

[Complete TSX file](/examples/form/reset.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import { Field, FieldLabel } from "@/siteplane/components/field";
import { Form } from "@/siteplane/components/form";
import { Input } from "@/siteplane/components/input";
import { useState } from "react";

export default function Example() {
  const [value, setValue] = useState("Mara");

  return (
    <Form
      className="grid w-full max-w-sm gap-3"
      onReset={() => {
        setValue("Mara");
      }}
      onSubmit={(event) => {
        event.preventDefault();
      }}
    >
      <Field name="name">
        <FieldLabel>Display name</FieldLabel>
        <Input value={value} onValueChange={setValue} />
      </Field>
      <div className="flex gap-2">
        <Button type="submit">Save</Button>
        <Button type="reset" variant="outline">
          Reset
        </Button>
      </div>
    </Form>
  );
}
```

## Floating labels

Set `labelPlacement="floating"` on each Field for a compact form whose labels sit inside the fields, such as an address form. See [Field](/docs/components/field/#floating-label) for the supported controls. Every field but VAT ID is [required](/docs/components/field/#required-fields) and marked with `requiredIndicator`. With `validationMode="onBlur"` on the Form, a field left empty turns invalid and shows its message in the [error tooltip](/docs/components/field/#error-tooltip) when you return to it, and a submit moves focus to the first invalid field.

Contact details form

[Complete TSX file](/examples/form/floating-labels.tsx)

```tsx
"use client";

import { Button } from "@/siteplane/components/button";
import {
  Field,
  FieldDescription,
  FieldError,
  FieldLabel,
} from "@/siteplane/components/field";
import {
  Fieldset,
  FieldsetDescription,
  FieldsetLegend,
} from "@/siteplane/components/fieldset";
import { Form } from "@/siteplane/components/form";
import { Input } from "@/siteplane/components/input";
import {
  Select,
  SelectItem,
  SelectPopup,
  SelectTrigger,
  SelectValue,
} from "@/siteplane/components/select";

const countries = [
  { label: "Germany", value: "de" },
  { label: "Austria", value: "at" },
  { label: "Switzerland", value: "ch" },
];

export default function Example() {
  return (
    <Form
      className="grid w-full max-w-sm gap-4"
      onSubmit={(event) => event.preventDefault()}
      validationMode="onBlur"
    >
      <Fieldset>
        <FieldsetLegend>Contact details</FieldsetLegend>
        <FieldsetDescription>
          Enter the details of the person or company.
        </FieldsetDescription>
        <div className="grid grid-cols-2 gap-3">
          <Field labelPlacement="floating" name="firstName" requiredIndicator>
            <FieldLabel>First name</FieldLabel>
            <Input autoComplete="given-name" required />
            <FieldError match="valueMissing">Enter your first name.</FieldError>
          </Field>
          <Field labelPlacement="floating" name="lastName" requiredIndicator>
            <FieldLabel>Last name</FieldLabel>
            <Input autoComplete="family-name" required />
            <FieldError match="valueMissing">Enter your last name.</FieldError>
          </Field>
        </div>
        <Field labelPlacement="floating" name="street" requiredIndicator>
          <FieldLabel>Street and number</FieldLabel>
          <Input autoComplete="street-address" required />
          <FieldError match="valueMissing">
            Enter your street and number.
          </FieldError>
        </Field>
        <div className="grid grid-cols-2 gap-3">
          <Field labelPlacement="floating" name="postcode" requiredIndicator>
            <FieldLabel>Postcode</FieldLabel>
            <Input autoComplete="postal-code" required />
            <FieldError match="valueMissing">Enter your postcode.</FieldError>
          </Field>
          <Field labelPlacement="floating" name="city" requiredIndicator>
            <FieldLabel>City</FieldLabel>
            <Input autoComplete="address-level2" required />
            <FieldError match="valueMissing">Enter your city.</FieldError>
          </Field>
        </div>
        <Field labelPlacement="floating" name="country" requiredIndicator>
          <FieldLabel>Country</FieldLabel>
          <Select items={countries} required>
            <SelectTrigger className="w-full">
              <SelectValue />
            </SelectTrigger>
            <SelectPopup>
              {countries.map((country) => (
                <SelectItem key={country.value} value={country.value}>
                  {country.label}
                </SelectItem>
              ))}
            </SelectPopup>
          </Select>
          <FieldError match="valueMissing">Choose your country.</FieldError>
        </Field>
        <Field labelPlacement="floating" name="vatId">
          <FieldLabel>VAT ID</FieldLabel>
          <Input />
          <FieldDescription>Optional.</FieldDescription>
        </Field>
        <Field labelPlacement="floating" name="phone" requiredIndicator>
          <FieldLabel>Phone</FieldLabel>
          <Input
            autoComplete="tel"
            placeholder="+49 123 4567"
            required
            type="tel"
          />
          <FieldError match="valueMissing">Enter your phone number.</FieldError>
        </Field>
      </Fieldset>
      <Button size="xl" type="submit">
        Save and continue
      </Button>
    </Form>
  );
}
```

## API reference

`Form` renders Base UI Form and adds `data-slot="form"`; on a native reset it clears the `errors` it was given. It adds no props of its own and accepts string className and all [Base UI Form props](https://base-ui.com/react/components/form).

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| validationMode | onSubmit \| onBlur \| onChange | onSubmit | Form-wide validation timing, overridden per Field. |
| errors | Record<string, string \| string[]> | unset | External messages keyed by Field name. A native reset clears them until you pass a new object. |
| onSubmit | FormEventHandler | unset | Native submission event; prevent default navigation for a local handler. |
| onFormSubmit | (values, eventDetails) => void | unset | Foundation submission callback; automatically prevents the native submit default. |
| onReset | FormEventHandler | unset | Reset application-owned controlled state here. Prevent the default to keep the values and the validation. |
| actionsRef | ref | unset | validate() validates all fields; validate(name) validates one. |
`FormPrimitive` exposes the foundation component. The Siteplane UI wrapper uses
its default Props type rather than adding a separately generic wrapper. Native
action/method, refs and composition are inherited. There is no built-in network
request, pending, success or error recovery service.

[Read the installed source](/source/form).

## Usage notes

Use type="submit" deliberately: ordinary kit Buttons default to button. Validate again at the server before persistence. Async Field.validate does not prevent onSubmit submission; awaiting a server check belongs in your submission handler.
