<!-- Protocore Design System — Form -->
# Form

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/form`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { Form, useForm } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/form

> Headless form-state hook (useForm) plus a lightweight Form context that wires labels, controls, and per-field errors.

## Two layers

The form system is deliberately split. **`useForm`** is a small, dependency-free hook that owns state — `values`, `errors`, `touched`, `dirty` — and exposes `setValue`, `reset`, `validate`, `getInputProps(name)`, and an `onSubmit(handler)` wrapper that gates on validation. **`Form.Root` / `Form.Field`** are the thin display layer: they put a `useForm` instance on context and render a labelled, error-aware wrapper around each control. Reach for the hook alone when you want full control of the markup; reach for the components when you want the wiring done for you.

## When to use it

Use **useForm** for local form state — a login box, a settings panel, a create dialog — where you want validation and dirty-tracking without a data-layer dependency. It is intentionally **not** a form engine: no field arrays, no async field validators, no schema resolver. For those, wire a dedicated library to your controls with `getInputProps` as the seam, or validate against a schema inside a single `validate` function.

## Examples

### Basics

Create an instance with `useForm({ initialValues, rules })`, hand it to `Form.Root`, and wrap each control in a `Form.Field` with a `name`. The field injects `value`/`onChange`/`onBlur`/`id` and surfaces the field's error once it has been touched.

```tsx
import { Form, useForm, Input, Button } from "@protocore/pds";

export default function Demo() {
  const form = useForm({
    initialValues: { project: "", region: "eu-central-1" },
  });

  return (
    <Form.Root
      form={form}
      onSubmit={(values) => alert(JSON.stringify(values, null, 2))}
      style={{ maxWidth: 380 }}
    >
      <Form.Field name="project" label="Project name" hint="Lowercase, no spaces.">
        <Input placeholder="my-service" />
      </Form.Field>
      <Form.Field name="region" label="Region">
        <Input />
      </Form.Field>
      <Button type="submit">Create</Button>
    </Form.Root>
  );
}
```

### Validation on submit

`onSubmit` runs validation first and only calls your handler when the form is valid; otherwise every field is marked touched and its error appears. Rules are per-field functions returning a message string (or nothing when valid).

```tsx
import { Form, useForm, Input, Button } from "@protocore/pds";

export default function Demo() {
  const form = useForm({
    initialValues: { email: "", password: "" },
    rules: {
      email: (v) => (!v.includes("@") ? "Enter a valid email address." : undefined),
      password: (v) => (v.length < 8 ? "At least 8 characters." : undefined),
    },
  });

  return (
    <Form.Root
      form={form}
      onSubmit={(values) => alert(`Signed in as ${values.email}`)}
      style={{ maxWidth: 380 }}
    >
      <Form.Field name="email" label="Email" required>
        <Input type="email" placeholder="you@example.com" />
      </Form.Field>
      <Form.Field name="password" label="Password" required>
        <Input type="password" />
      </Form.Field>
      <Button type="submit">Sign in</Button>
    </Form.Root>
  );
}
```

## Do & don't

**Do**

- Keep `initialValues` covering every field so `dirty` and `reset` behave.
- Return a message string from a rule when invalid, and a falsy value when valid.
- Let `onSubmit` gate the network call — it only fires the handler when valid.
- Give each `Form.Field` a stable `name` that exists in the values.

**Don't**

- Don't reach for a heavy form library when a page has one small form.
- Don't duplicate validation in the control and the rule — keep it in the rule.
- Don't read `errors` for display before a field is touched — use `Form.Field`, which waits.
- Don't mutate `values` directly — go through `setValue` / `setValues`.

## Accessibility

**Notes**

- `Form.Field` links its label to the control via `htmlFor`/`id` and points `aria-describedby` at the error.
- Field errors render in a `role="alert"` region so they are announced when they appear.
- The `<form>` uses `noValidate` so validation messaging is the component's, not the browser's.

## Related

`field`, `input`, `select`, `checkbox-group`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
