Skip to content
Protocore Design Systemv1.6.9

/// Inputs

Form

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

import { Form, useForm } from "@protocore/pds";
View as Markdown

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.

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.

Lowercase, no spaces.

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

Headless — hook only

Skip the components entirely: spread getInputProps(name) onto your own controls and read values / dirty directly. The hook never renders anything.

Value: · dirty: false

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.

Usage

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

  • `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

  • FieldForm-field wrapper: a caption label, the control, and hint or error messaging with ARIA wired for you.
  • InputThe base single-line text control — a sunken field with optional leading and trailing adornment slots.
  • SelectInput-styled trigger plus an overlay panel for choosing one value from a list; flat or compound API.
  • CheckboxGroupA set of checkboxes sharing one array value — the multi-select counterpart to RadioGroup.