Usage
form element and its submit contract. It draws no fields and sets no gap: stack Field, Fieldset, RadioGroup, and the actions with FlexBox or a form layout block.
Coming from ui-forms and react-hook-form? packages/ui/docs/migrating-from-ui-forms.md covers the Controller recipes, form.handleSubmit(fn) into onSubmit, and who owns FormProvider.
Submit stays native: Enter in a single-line field submits, and Enter in a textarea inserts a new line. Like ui-forms' Form, noValidate defaults to true, so the browser does not block the submit ahead of the validation you run in onSubmit. Pass noValidate={false} to let the browser validate and focus its first invalid control. Give the form an accessible name (aria-label or aria-labelledby) so it is a form landmark.
onSubmit receives the submit event; it plays the role of ui-forms' onValid, so run your validation there and set invalid or errorSummary when it fails. Call event.preventDefault() to replace the native navigation with an explicit action. Return a promise and Form stays pending until it settles, or control the state yourself with pending.
While pending, Form sets aria-busy, ignores further submits, and shares the state through useFormPending. The page is never covered or disabled. Pass the state to the submit control so the action reads as busy:
errorSummary renders a live region (role="alert") before the fields. It is announced when it appears and linked to the form through aria-describedby. The copy is yours; Form ships no string.
After a submit attempt settles with the form invalid, focus moves once, according to errorFocus:
FormErrorFocus.FirstInvalid (default): the first control marked aria-invalid="true", or the first focusable control inside an invalid group such as a radiogroup. With no such control, the summary.FormErrorFocus.Summary: the summary region.FormErrorFocus.None: focus stays; the summary announcement carries the error.errorSummary is set. Pass invalid when per-field errors exist without a summary. With noValidate={false}, native constraint validation focuses its own first invalid control before onSubmit runs.
A blur error caused by pressing Save is announced once, through the submit, not twice. Set invalid (or pass errorSummary) so a failed submit moves focus; without either, the submit speaks nothing and the fields announce their own errors. Errors that appear without a submit, such as blur or async validation, are announced politely once; Form owns no extra live region for them, because every control announces through the shared one. Validating on every keystroke announces every change, so validate on blur or debounce. Pass announceFieldErrors={false} when the page announces validation itself.
Focus moves for errors that exist when a submit attempt settles. For async work, return the promise from onSubmit or drive pending, so Form waits for the result before it moves focus.
Form tracks no values, so it has no dirty state, and it renders no Cancel or Save. Blocks that compose it (like ui-forms' StandardForm) own the footer. Save is not gated on a dirty form: it stays enabled when nothing changed, and the form validates on submit (packages/ui/docs/decisions/0023-forms-and-settings-composition.md, decision 13). Lay out fields in a FlexBox column at a 16px gap, with a footer row that is end aligned, 16px above, and 16px between actions.
onAfterReset runs after the form resets and no reset handler canceled it. Use it to clear error state Form does not own.