Form
An optional client <form> for forms whose errors come back from the server. Errors keyed by Field name appear in each Field, focus moves to the first invalid control, and an optional error summary links to each one. SubmitButton is pending while the form's action runs; FormMessage is the form-level message.
Import
import { Form, FormMessage, SubmitButton } from '@nizivo/react/form';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/alert.css and @nizivo/react/css/base.css and @nizivo/react/css/button.css
import '@nizivo/react/css/form.css';Static or interactive
Server, with two client parts. FormMessage renders on the server and ships no client
JavaScript; Form and SubmitButton are client leaves, imported from @nizivo/react/form beside
it. Every Field already works without Form: native validation is inline and focuses the first
invalid control, and an error passed to a Field's error shows with no client code of Form's. Use
Form when errors come back from a server action and you want them matched to Fields by name.
How to choose.
In a client app (a Vite or React Router single-page app, or a 'use client' file) it is an ordinary React component: event handlers, refs and state work as usual. In a client app.
Errors from a server action
Pass the action's field errors to errors, keyed by each Field's name, as
z.flattenError(error).fieldErrors returns them: a message, or a list of messages, of which the
first shows. A nested name is one key, written as the Field's name is (items.0.title). Each
error goes through the Field's one error channel: the Field's own error prop wins over it, and it
wins over a native validation message.
'use client';
const [state, action] = useActionState(saveProduct, {});
<Form action={action} errors={state.fieldErrors}>
<FormMessage>{state.formError}</FormMessage>
<Field name="title" label="Product title" required>
<Input defaultValue={state.values?.title} />
</Field>
<SubmitButton>Save</SubmitButton>
</Form>;- Put
nameon the Field, not only on the control: Form matches by the Field's name. A Field with a function child (zero JS) or aFieldStaticreads no Form errors; pass itserror. - Focus. Each new
errorsobject (by identity) moves focus to the first invalid control in page order, after the errors render, so the control is read with its message. The first render moves no focus, so a page that arrives with errors does not take focus as it loads. - Edits. Once the user changes a control's value, its error from the server is stale, so it is
hidden until the next
errorsobject. - Echo the values back as
defaultValue: React resets uncontrolled fields after an action, even one that returns errors.
Error summary
summary adds GOV.UK's error summary at the start of the form: a critical Alert titled "There is a
problem" (labels.summaryTitle) with one link per error, in page order. New errors focus the
summary instead of the first invalid control, and a link focuses its control. Use it on long forms,
where the first error may be off screen. Its links point at the controls once the form has hydrated.
SubmitButton
A primary Button with type="submit" that is pending while its form's action runs, read with
React's useFormStatus: the spinner shows, it stays focusable with aria-disabled, and it cannot
submit again. Only an action passed as a function (<Form action={fn}>) reports a status, never a
URL. pending forces the state, for example while a client handler awaits something else.
FormMessage
The form-level message, for failures no Field owns (network, permission, a conflict) and for
success. Render it always and change only its children: the live container is in the page from the
first render, empty, so a message added later is announced. critical (the default) is
role="alert"; success, info and warning are role="status".
Examples
Basic
Native validation, inline: submit with an empty title to see its message and focus move to it. With an action passed as a function, SubmitButton shows pending while it runs.
import type { JSX } from 'react';
import { Field } from '@nizivo/react/field';
import { Input } from '@nizivo/react/input';
import { Form, SubmitButton } from '@nizivo/react/form';
export function FormExample(): JSX.Element {
return (
<Form style={{ display: 'grid', gap: 16, maxInlineSize: 360 }}>
<Field
name="title"
label="Product title"
required
messages={{ valueMissing: 'Enter a product title.' }}
>
<Input />
</Field>
<Field name="sku" label="SKU" description="Letters and numbers, as printed on the label.">
<Input />
</Field>
<div>
<SubmitButton>Save</SubmitButton>
</div>
</Form>
);
}
Server Errors
A server action's reply: errors by Field name, presented by each Field until the user edits it, an error summary that links to each control, and a form-level FormMessage.
import type { JSX } from 'react';
import { Field } from '@nizivo/react/field';
import { Input } from '@nizivo/react/input';
import { Form, FormMessage, SubmitButton } from '@nizivo/react/form';
export function FormServerErrorsExample(): JSX.Element {
return (
<Form
summary
errors={{
title: 'Enter a product title.',
price: ['Enter a price above 0.', 'Enter a number.'],
}}
style={{ display: 'grid', gap: 16, maxInlineSize: 360 }}
>
<FormMessage>
We could not save the product. Check the fields below and try again.
</FormMessage>
<Field name="title" label="Product title" required>
<Input />
</Field>
<Field name="price" label="Price" required>
<Input inputMode="decimal" prefix="₹" defaultValue="0" />
</Field>
<div>
<SubmitButton>Save</SubmitButton>
</div>
</Form>
);
}
Message
The form-level message. Render it always, empty until there is something to say, so the message is announced when it arrives: critical as an alert, the other tones as a status.
import type { JSX } from 'react';
import { FormMessage } from '@nizivo/react/form';
export function FormMessageExample(): JSX.Element {
return (
<div style={{ display: 'grid', gap: 12, maxInlineSize: 420 }}>
<FormMessage tone="success">Product saved.</FormMessage>
<FormMessage tone="critical" title="Not saved">
Your session ended. Sign in again to save the product.
</FormMessage>
<FormMessage tone="info">{null}</FormMessage>
</div>
);
}
Props
Form
Client component Renders no element of its own. Props: FormProps. Source: src/form/form.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | – | |
errors | FormErrors | – | Errors by Field |
labels | Partial<FormLabels> | – | Overrides Form's strings, for example |
render | RenderProp<Record<string, never>> | – | Composition of the |
summary | boolean | false | Renders an error summary at the start of the form while |
- Also takes every attribute of
<form>exceptchildren. (Omit<ComponentPropsWithRef<"form">, "children">)
render takes an element, or, in client code, a function of the props and no state.
FormMessage
The form-level message: a live container that is always rendered, with an Alert inside it while there is a message. Field errors belong to each Field (error, or Form's errors), not here.
Server component Renders data-slot="form-message" with the class nz-form-message. Props: FormMessageProps. Source: src/form/form.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
action | ReactNode | – | A control under the message, for example |
children | ReactNode | – | The message. |
icon | ReactNode | – | Decorative tone glyph, as Alert's |
render | RenderProp<FormMessageState> | – | Composition of the live container. |
title | ReactNode | – | One line above the message, as Alert's |
tone | FormMessageTone"info""success""warning""critical" | 'critical' |
|
- Also takes every attribute of
<div>exceptrole,title;childrenis redeclared above. (Omit<ComponentPropsWithRef<"div">, "title" | "role">)
render takes an element, or, in client code, a function of the props and this state (FormMessageState):
| Name | Type | Default | Description |
|---|---|---|---|
tonerequired | FormMessageTone"info""success""warning""critical" | – |
SubmitButton
Client component Renders no element of its own. Props: SubmitButtonProps. Source: src/form/submit-button.client.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | – | From |
fullWidth | boolean | false | Fills the width of its container. From |
labels | Partial<ButtonLabels> | – | Overrides Button's strings, for example From |
leadingIcon | ReactNode | – | From |
pending | boolean | false | Pending even when the form's action is not running, for example while a client handler awaits. The form's own status is read for you: only an action passed as a function ( |
pendingIndicator | ReactNode | – | Replaces the internal spinner glyph. From |
render | RenderProp<ButtonState> | – | Composition, for example From |
size | ControlSize"sm""md""lg" | 'md' | 32 / 40 / 48 px. From |
trailingIcon | ReactNode | – | From |
variant | ButtonVariant"primary""secondary""tertiary""critical""accent" | 'primary' | The form's main action, so |
- Also takes every attribute of
<button>exceptchildren,type. (Omit<ComponentPropsWithRef<"button">, "children">)
render takes an element, or, in client code, a function of the props and this state (ButtonState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
pendingrequired | boolean | – | |
sizerequired | ControlSize"sm""md""lg" | – | |
variantrequired | ButtonVariant"primary""secondary""tertiary""critical""accent" | – |
Other exports
FormErrorValuetypeimport type { FormErrorValue } from '@nizivo/react/form'One Field's errors: a message, or several (the first is shown), or none.
type FormErrorValue = string | readonly string[] | null | undefinedFormErrorstypeimport type { FormErrors } from '@nizivo/react/form'Errors keyed by the Field's
name, as a server action returns them (Zod'sz.flattenError(error).fieldErrors, or Conform's and TanStack Form's issues mapped to text). A nested name is one key, written as the Field'snameis:items.0.title. Serialisable.type FormErrors = Readonly<Record<string, FormErrorValue>>FormLabelstypeimport type { FormLabels } from '@nizivo/react/form'Form's own strings (§16.4): serialisable, English defaults.
Name Type Default Description summaryTitlerequiredstring'There is a problem'The error summary's title.
FormMessagePropstypeimport type { FormMessageProps } from '@nizivo/react/form'The props of
FormMessage, listed above.FormMessageStatetypeimport type { FormMessageState } from '@nizivo/react/form'The render state of
FormMessage, listed above.FormMessageTonetypeimport type { FormMessageTone } from '@nizivo/react/form'Alert's tones.
criticalis arole="alert"container; the others arerole="status".type FormMessageTone = AlertToneFormPropstypeimport type { FormProps } from '@nizivo/react/form'The props of
Form, listed above.SubmitButtonPropstypeimport type { SubmitButtonProps } from '@nizivo/react/form'The props of
SubmitButton, listed above.
Data attributes and slots
Style parts by class (.nz-<slot>) and state by data attribute. Attribute meanings come from contracts/vocabulary.json.
| Slot | Rendered by | Data attributes |
|---|---|---|
form.nz-form | Forminner element (CSS only) | none |
form-message.nz-form-message | FormMessageroot (takes render) |
|
form-summary.nz-form-summary | Forminner element (CSS only); interactive, focus: self | none |
form-summary-link.nz-form-summary-link | Forminner element (CSS only); interactive, focus: self | none |
form-summary-list.nz-form-summary-list | Forminner element (CSS only) | none |
CSS hooks
Set a hook on the component's class or any ancestor to restyle it without specificity games; leave it unset for the default shown.
| Hook | Default | Read as |
|---|---|---|
--nz-form-summary-link-fg | --nz-text-critical |
|
Layers: nz.components. Classes: .nz-form-summary, .nz-form-summary-link, .nz-form-summary-list.
Tokens this stylesheet reads (5)
--nz-radius-lg, --nz-radius-xs, --nz-space-1, --nz-space-4, --nz-text-critical
Accessibility
These parts are interactive. Each gets the focus outline and touch-action rule from @nizivo/react/css/base.css (the nz.base layer), so the floor survives a restyle:
form-summary: draws the focus outline itself on keyboard focus.form-summary-link: draws the focus outline itself on keyboard focus.
Keyboard
| Keys | Action |
|---|---|
| Enter | In a text field, submits the form (the browser's implicit submission). Native validation runs first; a failed constraint shows its message in the Field and focuses the first invalid control. |
| Tab | Moves through the form's controls in document order. When an action returns errors, focus is already on the first invalid control, or on the error summary when summary is on. |
| Enter (on a summary link) | Focuses the control that error belongs to. |
Source: a native <form> (form.client.tsx) and SubmitButton's native <button type="submit">. The browser lane (test/browser/form.spec.tsx) checks native validation inside a Form, an action's errors by name with focus on the first invalid control, the summary's focus and links, and SubmitButton's pending state.
Figma
Node ids from design/component-map.json, which nz gen writes from component.json. The links open the Nizivo Figma library, which may ask you to sign in.
- Canonical: none
- Ghost masters: none
FileUpload
A dropzone over a native file input, with a list of the chosen files. Before any JavaScript runs, a click or a drop chooses files and a plain form submits them; hydrated, it checks each file against accept and maxSize and shows the upload state your code reports for each one.
Grid
The page grid: --nz-grid-columns columns (4, 8, 12 by viewport). Items snap to spans.