Skip to content
Nizivo UI

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.

Server component with client leaf Form, SubmitButton

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 name on the Field, not only on the control: Form matches by the Field's name. A Field with a function child (zero JS) or a FieldStatic reads no Form errors; pass its error.
  • Focus. Each new errors object (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 errors object.
  • 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.

Letters and numbers, as printed on the label.
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.

Enter a product title.
₹
Enter a price above 0.
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.

Product saved.
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.

NameTypeDefaultDescription
childrenReactNode–
errorsFormErrors–

Errors by Field name, typically a server action's reply. Each one is presented by the Field with that name (the Field's own error prop wins over it) until the user edits that control. A new object (by identity) presents them all again and moves focus: to the error summary with summary, else to the first invalid control. The first render moves no focus.

labelsPartial<FormLabels>–

Overrides Form's strings, for example labels={{ summaryTitle: 'Es gibt ein Problem' }}.

renderRenderProp<Record<string, never>>–

Composition of the <form>.

summarybooleanfalse

Renders an error summary at the start of the form while errors presents a message: a title and one link per error to its control, in page order. New errors focus the summary instead of the first invalid control (GOV.UK's error summary). For long forms.

  • Also takes every attribute of <form> except children. (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.

NameTypeDefaultDescription
actionReactNode–

A control under the message, for example <Button size="sm">Retry</Button>.

childrenReactNode–

The message. null, false and '' render nothing inside the container, which stays in the page so the next message is announced: render FormMessage always, and change only this.

iconReactNode–

Decorative tone glyph, as Alert's icon; the text carries the meaning.

renderRenderProp<FormMessageState>–

Composition of the live container.

titleReactNode–

One line above the message, as Alert's title.

toneFormMessageTone
"info""success""warning""critical"
'critical'

critical for a failure the user must act on (network, permission, conflict): role="alert". success, info and warning are role="status", read when the reader is idle.

  • Also takes every attribute of <div> except role, title; children is redeclared above. (Omit<ComponentPropsWithRef<"div">, "title" | "role">)

render takes an element, or, in client code, a function of the props and this state (FormMessageState):

NameTypeDefaultDescription
tonerequiredFormMessageTone
"info""success""warning""critical"
–

SubmitButton

Client component Renders no element of its own. Props: SubmitButtonProps. Source: src/form/submit-button.client.tsx.

NameTypeDefaultDescription
childrenReactNode–

From ButtonProps.

fullWidthbooleanfalse

Fills the width of its container.

From ButtonProps.

labelsPartial<ButtonLabels>–

Overrides Button's strings, for example labels={{ pending: 'Wird geladen…' }}; pending: '' shows no text and keeps the label visible. Translation is per instance: Button is a server part, so NizivoConfig labels does not reach it, and an app wraps Button once to pass its own. SubmitButton, its client twin, does read them (button.pending).

From ButtonProps.

leadingIconReactNode–

From ButtonProps.

pendingbooleanfalse

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 (<form action={fn}> or Form's) reports one, never a URL.

pendingIndicatorReactNode–

Replaces the internal spinner glyph.

From ButtonProps.

renderRenderProp<ButtonState>–

Composition, for example render={<Link href="/orders" />}.

From ButtonProps.

sizeControlSize
"sm""md""lg"
'md'

32 / 40 / 48 px.

From ButtonProps.

trailingIconReactNode–

From ButtonProps.

variantButtonVariant
"primary""secondary""tertiary""critical""accent"
'primary'

The form's main action, so primary; Button's other variants are accepted.

  • Also takes every attribute of <button> except children, type. (Omit<ComponentPropsWithRef<"button">, "children">)

render takes an element, or, in client code, a function of the props and this state (ButtonState):

NameTypeDefaultDescription
disabledrequiredboolean–
pendingrequiredboolean–
sizerequiredControlSize
"sm""md""lg"
–
variantrequiredButtonVariant
"primary""secondary""tertiary""critical""accent"
–

Other exports

FormErrorValue type

import 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 | undefined

FormErrors type

import type { FormErrors } from '@nizivo/react/form'

Errors keyed by the Field's name, as a server action returns them (Zod's z.flattenError(error).fieldErrors, or Conform's and TanStack Form's issues mapped to text). A nested name is one key, written as the Field's name is: items.0.title. Serialisable.

type FormErrors = Readonly<Record<string, FormErrorValue>>

FormLabels type

import type { FormLabels } from '@nizivo/react/form'

Form's own strings (§16.4): serialisable, English defaults.

NameTypeDefaultDescription
summaryTitlerequiredstring'There is a problem'

The error summary's title.

FormMessageProps type

import type { FormMessageProps } from '@nizivo/react/form'

The props of FormMessage, listed above.

FormMessageState type

import type { FormMessageState } from '@nizivo/react/form'

The render state of FormMessage, listed above.

FormMessageTone type

import type { FormMessageTone } from '@nizivo/react/form'

Alert's tones. critical is a role="alert" container; the others are role="status".

type FormMessageTone = AlertTone

FormProps type

import type { FormProps } from '@nizivo/react/form'

The props of Form, listed above.

SubmitButtonProps type

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

SlotRendered byData attributes
form
.nz-form
Form
inner element (CSS only)
none
form-message
.nz-form-message
FormMessage
root (takes render)
  • data-tone: "info""success""warning""critical" ← tone
    The tone prop; values in each component manifest
form-summary
.nz-form-summary
Form
inner element (CSS only); interactive, focus: self
none
form-summary-link
.nz-form-summary-link
Form
inner element (CSS only); interactive, focus: self
none
form-summary-list
.nz-form-summary-list
Form
inner 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.

HookDefaultRead as
--nz-form-summary-link-fg--nz-text-critical
  • color on .nz-form-summary-link

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

KeysAction
EnterIn 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.
TabMoves 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

On this page