Siteplane UI

Form

Submit related fields and display validation feedback.

Markdown
"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

pnpm dlx shadcn@4.18.0 add @siteplane/form

Complete the one-time setup first. Registry JSON lists source files and dependencies.

For this page’s examples, also install:

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

Usage

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

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

Composition

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.

Use a unique handle for your profile.

"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.

"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 for the supported controls. Every field but VAT ID is required and marked with requiredIndicator. With validationMode="onBlur" on the Form, a field left empty turns invalid and shows its message in the error tooltip when you return to it, and a submit moves focus to the first invalid field.

Contact details

Enter the details of the person or company.

Optional.

"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.

Component properties
PropTypeDefaultDescription
validationModeonSubmit | onBlur | onChangeonSubmitForm-wide validation timing, overridden per Field.
errorsRecord<string, string | string[]>unsetExternal messages keyed by Field name. A native reset clears them until you pass a new object.
onSubmitFormEventHandlerunsetNative submission event; prevent default navigation for a local handler.
onFormSubmit(values, eventDetails) => voidunsetFoundation submission callback; automatically prevents the native submit default.
onResetFormEventHandlerunsetReset application-owned controlled state here. Prevent the default to keep the values and the validation.
actionsRefrefunsetvalidate() 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.

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.