Photon Design System

Form

Coordinates a set of Fields under one <form> — it runs native constraint validation on submit, focuses the first invalid control, and maps server-side errors back onto the matching fields. Built on Base UI Form, it renders a <form noValidate> and owns validation itself. For the fields inside it, reach for Input (which wraps Field) or compose Base UI Field directly.

Import

import { Form } from "@photon-ai/pho-ui/components/form";

Basic

Compose Fields (or Inputs, which wrap Field) and a submit Button. Give every control a name — that’s the key Form uses to validate it and to route server errors back to it.

<Form onSubmit={handleSubmit}>
  <Input name="email" label="Email" type="email" required />
  <Button type="submit">Subscribe</Button>
</Form>

Submitting

On submit, Form validates every field first. If any is invalid it blocks the submission and focuses the first invalid control. If all pass, it calls your onSubmit; pass onFormSubmit instead to receive the parsed field values keyed by name (it calls preventDefault for you).

<Form
  onFormSubmit={(values) => {
    // values.email — collected from each field's `name`
    subscribe(values.email);
  }}
>
  …
</Form>

validationMode controls when fields validate: onSubmit (the default — validate on submit, then re-validate on change), onBlur, or onChange. A validationMode set on an individual Field wins over the form’s.

Server errors

Pass an errors object keyed by field name; Form flags the matching field invalid and, when the user next edits that field, clears its error automatically — there’s no clear-errors handler to wire. Rendering the message text requires a Field.Error part in the field, so with Photon’s Input drive its own invalid and hint props from the same error state:

<Form errors={serverErrors}>
  <Input
    name="email"
    label="Email"
    type="email"
    invalid={Boolean(serverErrors.email)}
    hint={serverErrors.email}
  />
  <Button type="submit">Subscribe</Button>
</Form>

Props

  • errors — Record<string, string | string[]> — external/server errors keyed by field name; each is cleared automatically when the user edits that field
  • onFormSubmit — (values, eventDetails) => void — fires after validation passes with the field values keyed by name; calls preventDefault
  • onSubmit — (event) => void — the native submit handler, called when validation passes (before onFormSubmit)
  • validationMode — onSubmit · onBlur · onChange (default onSubmit) — when contained fields validate
  • actionsRef — RefObject<{ validate(name?) }> — imperative handle to validate all fields, or one by name
  • className — string — class applied to the <form> element
  • …native <form> attributes — id, children, aria-*, … forwarded to the element

Accessibility

Form renders a native <form> element, so it inherits the platform’s form semantics; its job is to make validation legible to assistive tech.

  • Invalid fields — a field flagged by native validation or the errors prop is marked invalid on its control (aria-invalid), so screen readers announce it. Keep the reason in the field’s hint / Field.Error so the message is read out too.
  • Focus management — on a failed submit, Form moves focus to the first invalid control (and selects its text if it’s a text input), so the user lands on the problem without hunting.
  • Native validation — the form renders noValidate and runs constraints itself, keeping error timing consistent instead of relying on the browser’s built-in bubbles.

Keyboard: Enter in a field submits the form, which runs validation before any handler fires — so keep the submit Button typed submit and inside the Form.

Best practices

  • Give every control a name — it’s the key Form uses for validation and for routing server errors to the right field.
  • Return field-keyed errors from the server and let Form place them; don’t hand-roll a separate error banner for field-level problems.
  • Keep the submit Button inside the Form and typed submit, so Enter submits and validation runs before your handler.
  • Use onFormSubmit when you want the parsed values handed to you; use onSubmit when you need the raw event.