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 Pho’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 fieldname; each is cleared automatically when the user edits that field - onFormSubmit —
(values, eventDetails) => void— fires after validation passes with the field values keyed byname; callspreventDefault - onSubmit —
(event) => void— the native submit handler, called when validation passes (beforeonFormSubmit) - validationMode —
onSubmit·onBlur·onChange(defaultonSubmit) — when contained fields validate - actionsRef —
RefObject<{ validate(name?) }>— imperative handle to validate all fields, or one byname - 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
errorsprop is marked invalid on its control (aria-invalid), so screen readers announce it. Keep the reason in the field’shint/Field.Errorso the message is read out too. - Focus management — on a failed submit,
Formmoves 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
noValidateand 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 keyFormuses for validation and for routing server errors to the right field. - Return field-keyed
errorsfrom the server and letFormplace them; don’t hand-roll a separate error banner for field-level problems. - Keep the submit
Buttoninside theFormand typedsubmit, soEntersubmits and validation runs before your handler. - Use
onFormSubmitwhen you want the parsed values handed to you; useonSubmitwhen you need the raw event.