1
ComponentsForm Controls

Form

Form coordinates related fields, submission, validation, and server errors in one accessible workflow.

Purpose

Form coordinates related fields, submission, validation, and server errors in one accessible workflow.

Submit flow

Used for workspace notifications.

Not submitted
"use client";

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

export function Example() {
  return (
    <Form onSubmit={(event) => event.preventDefault()}>
      <Field name="email"><FieldLabel>Email</FieldLabel><Input /></Field>
      <Button type="submit">Save</Button>
    </Form>
  );
}

Installation

With the Siteplane registry alias configured, add the component and import the installed source from your app.

CLI

pnpm dlx shadcn@4.16.1 add @siteplane/form

Import

import { Form } from "@/components/ui/form";
import { Field, FieldDescription, FieldError, FieldLabel } from "@/components/ui/field";

Need the machine-readable source metadata? View registry JSON.

Usage

Use Form to wrap a submit flow. It connects field errors through names; use Field inside it for accessibility.

"use client";

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

export function Example() {
  return (
    <Form onSubmit={(event) => event.preventDefault()}>
      <Field name="email"><FieldLabel>Email</FieldLabel><Input /></Field>
      <Button type="submit">Save</Button>
    </Form>
  );
}

Examples

Validation, server errors, submission flows and layout patterns are compositions of the same primitive.

Required fields

Native required validation fills FieldError on submit.

Email: not submitted
<Form onSubmit={onSubmit}>
  <Field name="email">
    <FieldLabel>Work email</FieldLabel>
    <Input placeholder="you@acme.com" required type="email" />
    <FieldError />
  </Field>
  <Button loading={loading} type="submit">Submit</Button>
</Form>

Server errors

The errors prop maps server responses to fields by name. Submitting the handle "admin" is rejected.

Handle: not submitted
const [errors, setErrors] = useState<Record<string, string | string[]>>({});

async function onSubmit(event: FormEvent<HTMLFormElement>) {
  event.preventDefault();
  const handle = String(new FormData(event.currentTarget).get("handle") ?? "");
  setErrors({});
  await submitToServer(handle);
  if (handle === "admin") {
    setErrors({ handle: "This handle is already taken." });
  }
}

<Form errors={errors} onSubmit={onSubmit}>
  <Field name="handle">
    <FieldLabel>Public handle</FieldLabel>
    <Input defaultValue="admin" placeholder="acme" />
    <FieldError />
  </Field>
  <Button loading={loading} type="submit">Submit</Button>
</Form>

Loading submit

Invite: not submitted
<Form onSubmit={onSubmit}>
  <Field name="invite">
    <FieldLabel>Invite teammate</FieldLabel>
    <Input placeholder="teammate@acme.com" type="email" />
  </Field>
  <Button loading={loading} type="submit">Send invite</Button>
</Form>

Reset

Note: not submitted
<Form
  onReset={() => setStatus("Note: not submitted")}
  onSubmit={onSubmit}
>
  <Field name="note">
    <FieldLabel>Quick note</FieldLabel>
    <Input placeholder="Add a short note" />
  </Field>
  <div className="flex items-center gap-2">
    <Button type="submit">Submit</Button>
    <Button type="reset" variant="outline">Reset</Button>
  </div>
</Form>

Settings form

Profile details
Profile: not submitted
<Form onSubmit={onSubmit}>
  <Fieldset className="grid gap-3">
    <FieldsetLegend className="text-sm">Profile details</FieldsetLegend>
    <Field name="displayName">
      <FieldLabel>Display name</FieldLabel>
      <Input placeholder="Acme Team" />
    </Field>
    <Field name="company">
      <FieldLabel>Company</FieldLabel>
      <Input placeholder="Acme Inc." />
    </Field>
  </Fieldset>
  <Button loading={loading} type="submit">Save changes</Button>
</Form>

Inline form

Subscribe: not submitted
<Form onSubmit={onSubmit}>
  <Field name="subscribe">
    <FieldLabel className="sr-only" htmlFor={inputId}>
      Email address
    </FieldLabel>
    <div className="flex w-full items-center gap-2">
      <Input id={inputId} placeholder="you@acme.com" type="email" />
      <Button loading={loading} type="submit">Subscribe</Button>
    </div>
  </Field>
</Form>

API Reference

The reference lists the source-owned exports and props that are easy to miss in visual examples. Inherited Base UI props remain available unless the wrapper narrows them.

  • Give every Field a stable name.
  • errors accepts Record<string, string | string[]>; each key matches a Field name and renders through its FieldError.
  • Use the native onSubmit flow and real submit and reset Button controls.
  • Group related sections with Fieldset and FieldsetLegend.

Motion

Component motion uses the shared Siteplane motion contract. See the global motion guide for provider setup, tokens and reduced-motion behavior.

  • Form has no animation of its own.
  • Controls and Buttons retain their native focus, loading, validation, and reduced-motion behavior.
  • Reserve stable space or use concise feedback so validation does not move the entire workflow unexpectedly.

Accessibility

  • Every control requires a connected FieldLabel or accessible name.
  • Keep Field name, invalid state, Form errors, and FieldError synchronized.
  • Submit and reset Buttons need explicit visible action labels.
  • Move focus or provide an error summary when submission fails across several fields.

Implementation Guidance

  • Use Form for workflows with several related fields and shared submission or errors.
  • Do not use raw form controls without Field connections in visible app flows.
  • Keep server and client errors in the same Field error contract.

On This Page