Field
The label, help text and error around one form control. A server-safe shell; inside it, Nizivo controls pick up their id, name, flags and description wiring, and native validation messages show inline.
Import
import { Field, FieldStatic } from '@nizivo/react/field';
// Once per app (Getting started), after
// @nizivo/tokens/tokens.css and @nizivo/react/css/base.css and @nizivo/react/css/inline-error.css
import '@nizivo/react/css/field.css';Static or interactive
Split. A Field renders the FieldScope client leaf around any child except a function or a
NativeSelect placed directly in it. On those zero-JS paths no client code runs, but Turbopack and
next dev still ship FieldScope, because the shell imports it; FieldStatic, from the same import
path, imports no client module. 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.
import { FieldStatic } from '@nizivo/react/field';
import { NativeSelect } from '@nizivo/react/native-select';
<FieldStatic label="Country" name="country" required>
{(control) => (
<NativeSelect {...control}>
<option value="IN">India</option>
<option value="GB">United Kingdom</option>
</NativeSelect>
)}
</FieldStatic>;FieldStatic gives the function child the attributes to spread on the control, and as a second
argument the Field's ids, labelledBy and describedBy for a group or button control. Its child is
always a function (TypeScript rejects <FieldStatic><Input /></FieldStatic>, which would leave the
Input unwired). It shows no inline native validation message (the browser's bubble shows instead),
takes no messages or validateOn, and has no announcer. Around a Nizivo Input, Textarea,
Checkbox, Switch, SegmentedControl or Select, use Field: those controls read FieldScope, so the
page loads it anyway.
Validation in mixed forms
A Field shows the browser's validation message inline and moves focus to the first invalid control
it wires. A NativeSelect and a RadioGroup keep the browser's own message bubble, and the browser
moves focus to the first of those after the Field has moved it. In a form that mixes them, a failed
submit shows both kinds of message and focus can land past an earlier inline error. Until one channel
covers every control, keep a form on one kind, or validate on the server and pass error.
readOnly on a Field reaches Input and Textarea. Checkbox, Switch, SegmentedControl and NativeSelect
have no read-only state and ignore it, so the Field's data-readonly does not make them read-only.
Button controls
A control that is a <button> (Select, or a third-party picker) is named by
aria-labelledby={field.labelledBy}, and should call
useFieldControl({ labelBehavior: 'focus' }). A <label for> a button would press it and open its
popup; with the option, a click on the label focuses the control instead, with its focus ring, and
does nothing for a disabled control. Native controls need no option. The option is registered after
hydration, so the server HTML and the zero-JS path do not change.
Errors and screen readers
The error is not a live region. The control points at it (aria-describedby, aria-invalid), so
typing announces nothing, and on a failed submit the Field moves focus to the form's first invalid
control, which makes a screen reader read the control with its error.
When focus is already in that control, nothing moves: Enter pressed in a text field, a shortcut that
calls requestSubmit(), a Server Action that replies while the user waits in the field. For that case
alone, a Field that renders FieldScope (any child except a function child or a NativeSelect placed
directly in it) renders one visually hidden polite live region, after the error, and speaks the new
error once:
- It listens after a validation request made while focus is in the Field: the browser's
invalidevent at the form's first invalid control, or the form'ssubmit(any form library, or a Server Action). An app that clears its errors when a submit starts still has the reply spoken. - It speaks the next error text that renders, if focus is still in the Field: the browser's
message at once, even when the same message is already shown; for
error, the reply that changes the text (a pending re-render that keeps the old text says nothing). - It says nothing on page load and hydration, when the submit moves focus (the focus move reads the error), at a control that is not the form's first invalid one (focus goes to that one), while the user types, when a reply's text equals the error already shown, and to repeat the text it still holds.
- It reads the message only, not the error mark, and empties 7 seconds later.
The region is in the server HTML, empty, so it exists before any message does. A Field with a function child, or a NativeSelect placed directly in it (the zero-JS path), has none: there the browser's own message bubble speaks. A form library that moves focus into the control itself should do so after the error renders, or the control is read without it.
Bundle size with Turbopack
A zero-JS Field (a function child, or a NativeSelect placed directly in it) renders no client
component, and production webpack ships none of the Field's client code for it. Turbopack and
next dev ship every client module a page's server code imports, rendered or not, so such a page
still loads the Field's client leaf. Measured on the Next fixture (gzip of the page's client chunks,
a page with two zero-JS Fields, a function child and a NativeSelect placed directly, minus a page
with one Button; the cost is per module, not per Field): Turbopack production +3,364 B, production
webpack 0 B, next dev +8,551 B (unminified). FieldStatic avoids it: its module imports no
client code, so a page whose fields are all FieldStatic ships no FieldScope in any mode (the
fixture's /field-static page, the same two fields as FieldStatic: 0 B over the Button page under
Turbopack, webpack and next dev). A page
that also has a client control in a Field loads FieldScope anyway.
Examples
Basic
A client control inside a Field: Input reads the id, name, flags and description wiring through useFieldControl, and a failed submit shows the browser's validation message inline.
import type { JSX } from 'react';
import { Input } from '@nizivo/react/input';
import { Field } from '@nizivo/react/field';
export function FieldExample(): JSX.Element {
return (
<Field
label="Product title"
description="Shown on product pages and checkout."
required
messages={{ valueMissing: 'Enter a product title.' }}
>
<Input name="title" placeholder="e.g. Stoneware mug" />
</Field>
);
}
Error
An error returned by the server: icon and text under the help, joined to aria-describedby.
import type { JSX } from 'react';
import { Input } from '@nizivo/react/input';
import { Field } from '@nizivo/react/field';
export function FieldErrorExample(): JSX.Element {
return (
<Field label="Price" description="In rupees, before tax." error="Enter a price above zero.">
<Input name="price" inputMode="decimal" defaultValue="0" prefix="₹" />
</Field>
);
}
Zero Js
Zero JS: a NativeSelect placed in a Field, or any element through a function child ({(control) => <select {...control} />}), is wired as plain attributes, so a Server Component form ships no Field code for it.
import type { JSX } from 'react';
import { NativeSelect } from '@nizivo/react/native-select';
import { Field } from '@nizivo/react/field';
export function FieldZeroJsExample(): JSX.Element {
return (
<Field label="Country" description="Where the store ships from." required>
<NativeSelect name="country" placeholder="Choose a country">
<option value="IN">India</option>
<option value="SG">Singapore</option>
<option value="AE">United Arab Emirates</option>
</NativeSelect>
</Field>
);
}
Static
FieldStatic: the same label, help and error as Field, from a module with no client code, for a Server Component form. The function child spreads the wiring on the control itself.
import type { JSX } from 'react';
import { NativeSelect } from '@nizivo/react/native-select';
import { FieldStatic } from '@nizivo/react/field';
export function FieldStaticExample(): JSX.Element {
return (
<FieldStatic label="Ships from" description="Shown on the order confirmation." required>
{(control) => (
<NativeSelect {...control} name="origin" placeholder="Choose a warehouse">
<option value="blr">Bengaluru</option>
<option value="sin">Singapore</option>
<option value="dxb">Dubai</option>
</NativeSelect>
)}
</FieldStatic>
);
}
Props
Field
Server component Renders data-slot="field" with the class nz-field. Props: FieldProps. Source: src/field/field.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
children | ReactNode | ((control: FieldControlProps, field: FieldStaticWiring) => ReactNode) | – | The control. A function receives ready-made control attributes and renders with no client code at all (zero-JS mode, for Server-Component forms and plain elements); its second argument, |
description | ReactNode | – | Help text under the control, joined to its |
disabled | boolean | false | |
error | ReactNode | – | The error message, rendered through InlineError with an icon (never colour alone). Its presence is what "invalid" means (§8.3): |
errorIcon | ReactNode | – | Replaces the internal error mark ( |
group | boolean | false | The control is a group of native controls under one root (SegmentedControl, a third-party picker), not one labelable element: the label then has no |
ids | FieldIds | – | |
label | ReactNode | – | The visible name, a |
messages | Partial<Record<FieldValidityKey, string>> | – | Messages for native validation, by validity flag, instead of the browser's own text. |
name | string | – | Passed to the control. |
readOnly | boolean | false | |
render | RenderProp<FieldState> | – | Composition of the root |
required | boolean | false | The control is required: native |
validateOn | FieldValidateOn"submit""blur" | 'submit' | Native messages appear on submit, or also when the control loses focus. |
- Also takes every attribute of
<div>exceptchildren. (Omit<ComponentPropsWithRef<"div">, "children">)
render takes an element, or, in client code, a function of the props and this state (FieldState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
invalidrequired | boolean | – | An error message is presented. |
readOnlyrequired | boolean | – | |
requiredrequired | boolean | – |
FieldStatic
The Field anatomy with no client code: label, control, description and error, the same HTML and ids as Field with a function child (M31). Server-safe, and its module imports no client leaf, so a page that renders only FieldStatic ships no Field JavaScript in any bundler. It provides no context and no announcer, and native validation shows the browser's own bubble; the function child wires the control (FIELD-CONTRACT.md "FieldStatic").
Server component Renders data-slot="field" with the class nz-field. Props: FieldStaticProps. Source: src/field/field-static.tsx.
| Name | Type | Default | Description |
|---|---|---|---|
childrenrequired | (control: FieldControlProps, field: FieldStaticWiring) => ReactNode | – | The control, always a function: FieldStatic provides no context, so a Nizivo client control placed directly inside ( |
description | ReactNode | – | Help text under the control, joined to its From |
disabled | boolean | false | From |
error | ReactNode | – | The error message, rendered through InlineError with an icon (never colour alone). Its presence is what "invalid" means (§8.3): From |
errorIcon | ReactNode | – | Replaces the internal error mark ( From |
group | boolean | false | The control is a group of native controls under one root (SegmentedControl, a third-party picker), not one labelable element: the label then has no From |
ids | FieldIds | – | From |
label | ReactNode | – | The visible name, a From |
messages | never | – | Not on FieldStatic: nothing intercepts native validation, so the browser shows its own bubble. For inline native messages use |
name | string | – | Passed to the control. From |
readOnly | boolean | false | From |
render | RenderProp<FieldState> | – | Composition of the root From |
required | boolean | false | The control is required: native From |
validateOn | never | – | Not on FieldStatic: see |
- Also takes every attribute of
<div>exceptchildren. (Omit<ComponentPropsWithRef<"div">, "children">)
render takes an element, or, in client code, a function of the props and this state (FieldState):
| Name | Type | Default | Description |
|---|---|---|---|
disabledrequired | boolean | – | |
invalidrequired | boolean | – | An error message is presented. |
readOnlyrequired | boolean | – | |
requiredrequired | boolean | – |
Other exports
FieldControltypeimport type { FieldControl } from '@nizivo/react/field'Name Type Default Description controlPropsrequiredFieldControlElementProps– Spread on the native element, merged with the consumer's props through
mergeProps.describedByrequiredstring | undefined– disabledrequiredboolean– idrequiredstring | undefined– inFieldrequiredboolean– A Field is around the control.
invalidrequiredboolean– An error message is presented (
data-invalidsemantics, §8.3).labelledByrequiredstring | undefined– The Field label's id, for a control named by
aria-labelledby(button or group controls).namerequiredstring | undefined– readOnlyrequiredboolean– requiredrequiredboolean– FieldControlElementPropstypeimport type { FieldControlElementProps } from '@nizivo/react/field'controlProps: the attributes plus the Field's validation handlers, inside a Field.type FieldControlElementProps = FieldControlProps & { onInvalid?: ((event: SyntheticEvent<FieldValidatable>) => void) | undefined; onInput?: ((event: SyntheticEvent<FieldValidatable>) => void) | undefined; onBlur?: ((event: FocusEvent<FieldValidatable>) => void) | undefined; }FieldControlOptionstypeimport type { FieldControlOptions } from '@nizivo/react/field'What a control passes in: its own props, which win over the Field's (FIELD-CONTRACT.md).
Name Type Default Description aria-describedbystring– Joined after the Field's description and error ids, for example a text affix or a count.
disabledboolean– idstring– invalidboolean– labelBehavior"focus"– 'focus': a click on the Field label focuses the control and does nothing else. For button controls (Select, third-party pickers): a<label for>a button presses it, which would open its popup. Omitted, the label keeps the browser's behaviour.namestring– readOnlyboolean– requiredboolean– FieldControlPropstypeimport type { FieldControlProps } from '@nizivo/react/field'Attributes for the native element of a control: what a function child receives, and the base of
useFieldControl().controlProps. Serialisable; flags aretrueorundefined, neverfalse.type FieldControlProps = { id?: string | undefined; name?: string | undefined; required?: boolean | undefined; disabled?: boolean | undefined; readOnly?: boolean | undefined; "aria-describedby"?: string | undefined; "aria-invalid"?: boolean | undefined; }FieldIdstypeimport type { FieldIds } from '@nizivo/react/field'Ids a Field generates; each one given here replaces the generated one (Conform, tests).
Name Type Default Description controlstring– descriptionstring– errorstring– labelstring– FieldPropstypeimport type { FieldProps } from '@nizivo/react/field'The props of
Field, listed above.FieldStatetypeimport type { FieldState } from '@nizivo/react/field'The render state of
Field, listed above.FieldStaticPropstypeimport type { FieldStaticProps } from '@nizivo/react/field'The props of
FieldStatic, listed above.FieldStaticWiringtypeimport type { FieldStaticWiring } from '@nizivo/react/field'What a FieldStatic function child receives as its second argument: the wiring FieldStatic does not do for you, since it provides no context. Use it for a control that is not one labelable element (
aria-labelledbyon a group root or a button) or to point other elements at the parts.Name Type Default Description describedByrequiredstring | undefined– The description and error ids that render, in that order; the control's
aria-describedby.idsrequired{ control: string; label: string; description: string; error: string; }– Every id the Field renders, generated or taken from
ids.labelledByrequiredstring | undefined– The label's id while a label renders, for
aria-labelledby;undefinedwithout a label.FieldValidatabletypeimport type { FieldValidatable } from '@nizivo/react/field'A native element that takes part in constraint validation.
type FieldValidatable = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElementFieldValidateOntypeimport type { FieldValidateOn } from '@nizivo/react/field'When native validation messages are presented.
type FieldValidateOn = "submit" | "blur"FieldValidityKeytypeimport type { FieldValidityKey } from '@nizivo/react/field'The
ValidityStateflags amessagesentry can replace.type FieldValidityKey = "valueMissing" | "typeMismatch" | "patternMismatch" | "tooLong" | "tooShort" | "rangeUnderflow" | "rangeOverflow" | "stepMismatch" | "badInput" | "customError"useFieldControlhookimport { useFieldControl } from '@nizivo/react/field'Field wiring for a control (FIELD-CONTRACT.md). The control's own props win;
aria-describedbyis joined. With no Field around the control, everything comes fromoptions.function useFieldControl(options?: FieldControlOptions): FieldControl
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 |
|---|---|---|
field.nz-field | Fieldroot (takes render) |
|
field-description.nz-field-description | Fieldinner element (CSS only) | none |
field-error.nz-field-error | Fieldinner element (CSS only) | none |
field-label.nz-field-label | Fieldinner element (CSS only) | none |
field-required.nz-field-required | Fieldinner 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-field-description-fg | --nz-text-tertiary |
|
--nz-field-description-font | --nz-font-body-m |
|
--nz-field-gap | --nz-space-1-5 |
|
--nz-field-label-fg |
|
|
--nz-field-label-font | --nz-font-label-m |
|
--nz-field-required-fg | --nz-text-critical |
|
Layers: nz.components. Classes: .nz-field, .nz-field-description, .nz-field-label, .nz-field-required.
Tokens this stylesheet reads (8)
--nz-font-body-m, --nz-font-label-m, --nz-space-0-5, --nz-space-1-5, --nz-text-critical, --nz-text-disabled, --nz-text-primary, --nz-text-tertiary
Accessibility
Nothing in this component takes focus or keyboard input.
Keyboard
None.
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