# Form (/docs/form)





## API Reference [#api-reference]

<BaseUIReference href="https://base-ui.com/react/components/form" />

## Basic [#basic]

<Preview cssProperties="formPlaygroundCssProperties">
  <FormExample />

  <Preview.Code>
    {`
          import {
            type FormErrors,
            Form,
            Field,
            FieldLabel,
            Input,
            FieldError,
            Button,
          } from "moduix";
          import { useState } from "react";

          async function validateHomepage(homepage: string) {
            await new Promise((resolve) => {
              setTimeout(resolve, 500);
            });

            try {
              const value = new URL(homepage);

              if (value.hostname.endsWith("example.com")) {
                return {
                  homepage: "The example.com domain is not allowed.",
                };
              }

              return {};
            } catch {
              return {
                homepage: "Please enter a valid URL.",
              };
            }
          }

          export function FormDemo() {
            const [errors, setErrors] = useState({} as FormErrors);
            const [submitting, setSubmitting] = useState(false);

            return (
              <Form
                errors={errors}
                className={styles.form}
                onSubmit={async (event) => {
                  event.preventDefault();
                  const formData = new FormData(event.currentTarget);
                  const homepage = String(formData.get("homepage") ?? "");

                  setSubmitting(true);
                  const nextErrors = await validateHomepage(homepage);
                  setErrors(nextErrors);
                  setSubmitting(false);
                }}
              >
                <Field name="homepage" validationMode="onBlur">
                  <FieldLabel>Homepage</FieldLabel>
                  <Input
                    type="url"
                    required
                    defaultValue="https://example.com"
                    placeholder="https://example.com"
                    pattern="https?://.*"
                  />
                  <FieldError match="valueMissing">Please enter a homepage URL.</FieldError>
                  <FieldError match="patternMismatch">
                    Please start with http:// or https://.
                  </FieldError>
                  <FieldError />
                </Field>
                <Button type="submit" loading={submitting}>
                  Submit
                </Button>
              </Form>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .form {
            width: min(20rem, 100%);
          }
        `}
  </Preview.CSS>

  <Preview.CSSProperties>
    {(context) => <FormCssPropertiesPanel {...context} />}
  </Preview.CSSProperties>

  <Preview.CSSPlayground>
    {(context) => <FormCssPlaygroundPanel {...context} />}
  </Preview.CSSPlayground>
</Preview>

## Anatomy [#anatomy]

`Form` is composed as a root wrapper around native form semantics and validation orchestration.
Compose it with `Field` parts and submit controls inside the same root.

```text
Form
├─ Field
│  ├─ FieldLabel
│  ├─ control (Input, FieldControl, etc.)
│  └─ FieldError / FieldDescription
└─ submit actions (for example Button[type="submit"])
```

| Part   | Role                                                                      |
| ------ | ------------------------------------------------------------------------- |
| `Form` | Native form root that coordinates submit lifecycle and form-level errors. |

## Composition [#composition]

Use `Form` props such as `errors`, `actionsRef`, `onFormSubmit`, `validationMode`, `action`,
and native form attributes.
Compose fields with `Field`, `FieldLabel`, `Input`, `FieldDescription`, and `FieldError`.
`Form` does not hide service slots such as `Positioner`, `Backdrop`, or `Viewport`, so
customization is done directly through `className` on the form and composed child slots.

## Examples [#examples]

### On Form Submit [#on-form-submit]

Use `onFormSubmit` when you want form values as an object instead of reading `FormData` manually. Base UI prevents the native submit event for this handler.

<Preview>
  <FormOnFormSubmitExample />

  <Preview.Code>
    {`
          import {
            type FormErrors,
            Form,
            Field,
            FieldLabel,
            Input,
            FieldError,
            Button,
          } from "moduix";
          import { useState } from "react";

          export function OnFormSubmitDemo() {
            const [errors, setErrors] = useState({} as FormErrors);
            const [submitting, setSubmitting] = useState(false);

            return (
              <Form
                errors={errors}
                validationMode="onBlur"
                className={styles.form}
                onFormSubmit={(values) => {
                  setSubmitting(true);

                  const nextErrors = {} as FormErrors;
                  const age = Number(values.age);

                  if (!values.name?.trim()) {
                    nextErrors.name = "Name is required.";
                  }

                  if (!values.age?.trim()) {
                    nextErrors.age = "Age is required.";
                  } else if (!Number.isFinite(age) || age < 18) {
                    nextErrors.age = "Age should be 18 or greater.";
                  }

                  setErrors(nextErrors);
                  setSubmitting(false);
                }}
              >
                <Field name="name">
                  <FieldLabel>Name</FieldLabel>
                  <Input placeholder="Enter your name" />
                  <FieldError />
                </Field>
                <Field name="age">
                  <FieldLabel>Age</FieldLabel>
                  <Input type="number" placeholder="18" />
                  <FieldError />
                </Field>
                <Button type="submit" loading={submitting}>
                  Submit
                </Button>
              </Form>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .form {
            width: min(20rem, 100%);
          }
        `}
  </Preview.CSS>
</Preview>

### Actions Ref [#actions-ref]

Pass `actionsRef` when another control needs to trigger validation for one field or the whole form.

<Preview>
  <FormActionsRefExample />

  <Preview.Code>
    {`
          import {
            type FormActions,
            type FormErrors,
            Form,
            Field,
            FieldLabel,
            Input,
            FieldError,
            Button,
          } from "moduix";
          import { useRef, useState } from "react";

          export function ActionsRefForm() {
            const actionsRef = useRef(null as FormActions | null);
            const [errors, setErrors] = useState({} as FormErrors);

            return (
              <Form
                actionsRef={actionsRef}
                errors={errors}
                validationMode="onSubmit"
                className={styles.form}
                onFormSubmit={(values) => {
                  const nextErrors = {} as FormErrors;
                  const email = String(values.email ?? "");

                  if (!email.trim()) {
                    nextErrors.email = "Email is required.";
                  } else if (!email.endsWith("@2gis.com")) {
                    nextErrors.email = "Use a @2gis.com email.";
                  }

                  setErrors(nextErrors);
                }}
              >
                <Field name="email">
                  <FieldLabel>Work Email</FieldLabel>
                  <Input type="email" required placeholder="name@2gis.com" />
                  <FieldError />
                </Field>
                <Button
                  type="button"
                  variant="outline"
                  onClick={() => {
                    actionsRef.current?.validate("email");
                  }}
                >
                  Validate Email
                </Button>
                <Button type="submit">Submit</Button>
              </Form>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .form {
            width: min(20rem, 100%);
          }
        `}
  </Preview.CSS>
</Preview>

### Action State [#action-state]

Use a form `action` with React `useActionState` when server or action results should feed back into field errors.

<Preview>
  <FormActionStateExample />

  <Preview.Code>
    {`
          import {
            type FormErrors,
            Form,
            Field,
            FieldLabel,
            Input,
            FieldError,
            Button,
          } from "moduix";
          import { useActionState } from "react";

          interface ActionState {
            serverErrors?: FormErrors;
            message?: string;
          }

          async function submitUsername(
            _previousState: ActionState,
            formData: FormData,
          ) {
            await new Promise((resolve) => {
              setTimeout(resolve, 500);
            });

            const username = String(formData.get("username") ?? "");

            if (!username.trim()) {
              return {
                serverErrors: {
                  username: "Username is required.",
                },
              };
            }

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

            return {
              serverErrors: {},
              message: \`Saved as \${username}.\`,
            };
          }

          export function ActionStateForm() {
            const [state, formAction, loading] = useActionState(
              submitUsername,
              {} as ActionState,
            );

            return (
              <Form
                action={formAction}
                errors={state.serverErrors}
                validationMode="onBlur"
                className={styles.form}
              >
                <Field name="username">
                  <FieldLabel>Username</FieldLabel>
                  <Input required defaultValue="admin" placeholder="e.g. alice132" />
                  <FieldError />
                </Field>
                <Button type="submit" loading={loading}>
                  Submit
                </Button>
                {state.message ? <p className={styles.helper}>{state.message}</p> : null}
              </Form>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .form {
            width: min(20rem, 100%);
          }

          .helper {
            margin: 0;
            color: var(--color-muted-foreground);
            font-size: var(--text-sm);
            line-height: var(--line-height-text-sm);
          }
        `}
  </Preview.CSS>
</Preview>

### Custom Styles [#custom-styles]

Pass `className` to the form root and every composed field, input, error, and button slot when styling with CSS Modules, Tailwind CSS, or CSS-in-JS.

<Preview>
  <CustomStylesFormExample />

  <Preview.Code>
    {`
          import {
            Form,
            Field,
            FieldLabel,
            Input,
            FieldDescription,
            FieldError,
            Button,
          } from "moduix";

          export function CustomStylesFormDemo() {
            return (
              <Form validationMode="onBlur" className={styles.customForm}>
                <Field name="project" className={styles.customField}>
                  <FieldLabel className={styles.customLabel}>Project</FieldLabel>
                  <Input
                    required
                    placeholder="Maps Platform"
                    className={styles.customInput}
                  />
                  <FieldDescription className={styles.customDescription}>
                    Use the public project name.
                  </FieldDescription>
                  <FieldError className={styles.customError} match="valueMissing">
                    Please enter a project name.
                  </FieldError>
                </Field>
                <Button
                  type="submit"
                  variant="outline"
                  className={styles.customButton}
                >
                  Submit
                </Button>
              </Form>
            );
          }
        `}
  </Preview.Code>

  <Preview.CSS>
    {`
          .customForm {
            width: 20rem;
            max-width: 100%;
            gap: var(--spacing-3);
            padding: var(--spacing-4);
            border: var(--border-width-sm) solid color-mix(in srgb, var(--color-primary) 30%, transparent);
            border-radius: var(--radius-lg);
          }

          .customField {
            gap: var(--spacing-2);
          }

          .customLabel,
          .customError,
          .customButton {
            color: var(--color-primary);
          }

          .customInput,
          .customButton {
            border-color: color-mix(in srgb, var(--color-primary) 40%, transparent);
          }

          .customInput:focus {
            outline-color: var(--color-primary);
          }

          .customDescription {
            color: var(--color-muted-foreground);
          }
        `}
  </Preview.CSS>
</Preview>
