---
title: Form
description: A native form element with consolidated error handling.
links:
  doc: https://base-ui.com/react/components/form
  anatomy: https://base-ui.com/react/components/form#anatomy
  api: https://base-ui.com/react/components/form#api-reference
---

```tsx
"use client";

import { useState } 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 { Spinner } from "@/components/ui/spinner";

export function FormDemo() {
  const [loading, setLoading] = useState(false);
  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    setLoading(true);
    await new Promise((r) => setTimeout(r, 800));
    setLoading(false);
    alert(`Email: ${formData.get("email") || ""}`);
  };

  return (
    <Form className="flex w-full max-w-64 flex-col gap-4" onSubmit={onSubmit}>
      <Field name="email">
        <FieldLabel>Email</FieldLabel>
        <Input type="email" placeholder="you@example.com" required />
        <FieldError>Please enter a valid email.</FieldError>
      </Field>
      <Button type="submit" disabled={loading} focusableWhenDisabled>
        {loading && <Spinner />}
        Submit
      </Button>
    </Form>
  );
}
```

## Installation

<ComponentSource name="form" title="components/ui/form.tsx" />

## Usage

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

```tsx
<Form
  className="flex w-full flex-col gap-4"
  onSubmit={(e) => {
    /* handle submit */
  }}>
  <Field>
    <FieldLabel>Email</FieldLabel>
    <Input name="email" type="email" required />
    <FieldError>Please enter a valid email.</FieldError>
  </Field>
</Form>
```

## Examples

### Submit with server function

```tsx
"use client";

import { useActionState } 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";

interface FormState {
  serverErrors?: React.ComponentProps<typeof Form>["errors"];
}

export default function ActionStateForm() {
  const [state, formAction, loading] = useActionState<FormState, FormData>(submitForm, {});

  return (
    <Form
      errors={state.serverErrors}
      action={formAction}
      className="flex w-full max-w-64 flex-col gap-4"
    >
      <Field name="username">
        <FieldLabel>Username</FieldLabel>
        <Input type="username" required defaultValue="admin" placeholder="e.g. alice132" />
        <FieldError />
      </Field>
      <Button type="submit" disabled={loading} focusableWhenDisabled>
        {loading ? "Submitting..." : "Submit"}
      </Button>
    </Form>
  );
}

async function submitForm(_previousState: FormState, formData: FormData) {
  // Mimic a server response delay.
  await new Promise((resolve) => {
    setTimeout(resolve, 1000);
  });

  try {
    const username = formData.get("username") as string | null;

    if (username === "admin") {
      return { success: false, serverErrors: { username: "'admin' is reserved for system use" } };
    }

    // 50% chance the username is taken
    const success = Math.random() > 0.5;

    if (!success) {
      return {
        serverErrors: { username: `${username} is unavailable` },
      };
    }
  } catch {
    return { serverErrors: { username: "A server error has occurred" } };
  }

  return {};
}
```

### Submit form values as JavaScript object

```tsx title="form-demo.tsx"
<Form
  onFormSubmit={async (formValues: { id: string; quantity: number }) => {
    const payload = {
      product_id: formValues.id,
      order_quantity: formValues.quantity,
    };

    const response = await fetch("https://api.example.com", {
      method: "POST",
      body: payload,
    });
  }}
/>
```

### Using with zod

```tsx
"use client";

import { useState } from "react";
import { z } from "zod";

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

const formSchema = z.object({
  name: z.string().min(1, "Name is required"),
  age: z.coerce.number("Age must be a number").positive("Age must be a positive number"),
});

async function submitForm(formValues: FormPrimitive.Values) {
  const result = formSchema.safeParse(formValues);

  if (!result.success) {
    return {
      errors: z.flattenError(result.error).fieldErrors,
    };
  }

  return {
    errors: {},
  };
}

export function FormDemo() {
  const [errors, setErrors] = useState({});

  return (
    <Form
      errors={errors}
      onFormSubmit={async (formValues) => {
        const response = await submitForm(formValues);
        setErrors(response.errors);
      }}
      className="flex w-full max-w-64 flex-col gap-4"
    >
      <Field name="name">
        <FieldLabel>Name</FieldLabel>
        <Input type="text" placeholder="Enter your name" required />
        <FieldError>{formSchema.shape.name.safeParse("").error?.issues[0].message}</FieldError>
      </Field>
      <Field name="age">
        <FieldLabel>Age</FieldLabel>
        <Input type="number" placeholder="Enter your age" required />
        <FieldError>{formSchema.shape.age.safeParse("").error?.issues[0].message}</FieldError>
      </Field>
      <Button type="submit">Submit</Button>
    </Form>
  );
}
```
