Skip to content
Nizivo UI

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.

Server component with client leaf useFieldControl

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 invalid event at the form's first invalid control, or the form's submit (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.

Shown on product pages and checkout.
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.

₹
In rupees, before tax.
Enter a price above zero.
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.

Where the store ships from.
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.

Shown on the order confirmation.
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.

NameTypeDefaultDescription
childrenReactNode | ((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, field, carries the ids for anything else, as on FieldStatic. A NativeSelect placed directly inside is wired the same way, with no function. Client controls (Input, Checkbox, …) read the wiring through useFieldControl.

descriptionReactNode–

Help text under the control, joined to its aria-describedby.

disabledbooleanfalse
errorReactNode–

The error message, rendered through InlineError with an icon (never colour alone). Its presence is what "invalid" means (§8.3): aria-invalid on the control and data-invalid on the Field. It is not a live region; around a client control, the Field's one announcer speaks a new error while focus is already in the Field (§16.5's one exception, FIELD-CONTRACT.md). null, false and '' present no error.

errorIconReactNode–

Replaces the internal error mark (circle-x). Decorative.

groupbooleanfalse

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 for, and names the group through the aria-labelledby the control puts on its root (FIELD-CONTRACT.md).

idsFieldIds–
labelReactNode–

The visible name, a <label for> the control.

messagesPartial<Record<FieldValidityKey, string>>–

Messages for native validation, by validity flag, instead of the browser's own text.

namestring–

Passed to the control.

readOnlybooleanfalse
renderRenderProp<FieldState>–

Composition of the root <div>.

requiredbooleanfalse

The control is required: native required, and an aria-hidden mark after the label.

validateOnFieldValidateOn
"submit""blur"
'submit'

Native messages appear on submit, or also when the control loses focus.

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

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

NameTypeDefaultDescription
disabledrequiredboolean–
invalidrequiredboolean–

An error message is presented.

readOnlyrequiredboolean–
requiredrequiredboolean–

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.

NameTypeDefaultDescription
childrenrequired(control: FieldControlProps, field: FieldStaticWiring) => ReactNode–

The control, always a function: FieldStatic provides no context, so a Nizivo client control placed directly inside (<Input />) would not be wired, and that is a type error here (use Field). Spread control on the element that holds the value (<input {...control} />, <NativeSelect {...control}>); field carries the ids for anything else.

descriptionReactNode–

Help text under the control, joined to its aria-describedby.

From FieldProps.

disabledbooleanfalse

From FieldProps.

errorReactNode–

The error message, rendered through InlineError with an icon (never colour alone). Its presence is what "invalid" means (§8.3): aria-invalid on the control and data-invalid on the Field. It is not a live region; around a client control, the Field's one announcer speaks a new error while focus is already in the Field (§16.5's one exception, FIELD-CONTRACT.md). null, false and '' present no error.

From FieldProps.

errorIconReactNode–

Replaces the internal error mark (circle-x). Decorative.

From FieldProps.

groupbooleanfalse

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 for, and names the group through the aria-labelledby the control puts on its root (FIELD-CONTRACT.md).

From FieldProps.

idsFieldIds–

From FieldProps.

labelReactNode–

The visible name, a <label for> the control.

From FieldProps.

messagesnever–

Not on FieldStatic: nothing intercepts native validation, so the browser shows its own bubble. For inline native messages use Field.

namestring–

Passed to the control.

From FieldProps.

readOnlybooleanfalse

From FieldProps.

renderRenderProp<FieldState>–

Composition of the root <div>.

From FieldProps.

requiredbooleanfalse

The control is required: native required, and an aria-hidden mark after the label.

From FieldProps.

validateOnnever–

Not on FieldStatic: see messages.

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

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

NameTypeDefaultDescription
disabledrequiredboolean–
invalidrequiredboolean–

An error message is presented.

readOnlyrequiredboolean–
requiredrequiredboolean–

Other exports

FieldControl type

import type { FieldControl } from '@nizivo/react/field'

NameTypeDefaultDescription
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-invalid semantics, §8.3).

labelledByrequiredstring | undefined–

The Field label's id, for a control named by aria-labelledby (button or group controls).

namerequiredstring | undefined–
readOnlyrequiredboolean–
requiredrequiredboolean–
FieldControlElementProps type

import 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; }

FieldControlOptions type

import type { FieldControlOptions } from '@nizivo/react/field'

What a control passes in: its own props, which win over the Field's (FIELD-CONTRACT.md).

NameTypeDefaultDescription
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–
FieldControlProps type

import 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 are true or undefined, never false.

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; }

FieldIds type

import type { FieldIds } from '@nizivo/react/field'

Ids a Field generates; each one given here replaces the generated one (Conform, tests).

NameTypeDefaultDescription
controlstring–
descriptionstring–
errorstring–
labelstring–
FieldProps type

import type { FieldProps } from '@nizivo/react/field'

The props of Field, listed above.

FieldState type

import type { FieldState } from '@nizivo/react/field'

The render state of Field, listed above.

FieldStaticProps type

import type { FieldStaticProps } from '@nizivo/react/field'

The props of FieldStatic, listed above.

FieldStaticWiring type

import 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-labelledby on a group root or a button) or to point other elements at the parts.

NameTypeDefaultDescription
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; undefined without a label.

FieldValidatable type

import type { FieldValidatable } from '@nizivo/react/field'

A native element that takes part in constraint validation.

type FieldValidatable = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement

FieldValidateOn type

import type { FieldValidateOn } from '@nizivo/react/field'

When native validation messages are presented.

type FieldValidateOn = "submit" | "blur"

FieldValidityKey type

import type { FieldValidityKey } from '@nizivo/react/field'

The ValidityState flags a messages entry can replace.

type FieldValidityKey = "valueMissing" | "typeMismatch" | "patternMismatch" | "tooLong" | "tooShort" | "rangeUnderflow" | "rangeOverflow" | "stepMismatch" | "badInput" | "customError"

useFieldControl hook

import { useFieldControl } from '@nizivo/react/field'

Field wiring for a control (FIELD-CONTRACT.md). The control's own props win; aria-describedby is joined. With no Field around the control, everything comes from options.

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.

SlotRendered byData attributes
field
.nz-field
Field
root (takes render)
  • data-disabled (present or absent) ← disabled
    Mirrors the disabled attribute
  • data-invalid (present or absent)
    An error message is currently presented (not "fails constraints"); always the Field's own state, never the engine's
  • data-readonly (present or absent)
    Mirrors the readonly attribute
  • data-required (present or absent) ← required
    Mirrors the required attribute
  • data-validate-on: "submit""blur" ← validateOn
    When validation messages are presented
field-description
.nz-field-description
Field
inner element (CSS only)
none
field-error
.nz-field-error
Field
inner element (CSS only)
none
field-label
.nz-field-label
Field
inner element (CSS only)
none
field-required
.nz-field-required
Field
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-field-description-fg--nz-text-tertiary
  • color on .nz-field-description
--nz-field-description-font--nz-font-body-m
  • font on .nz-field-description
--nz-field-gap--nz-space-1-5
  • gap on .nz-field
--nz-field-label-fg
  • default: var(--nz-text-primary)
  • data-disabled: var(--nz-text-disabled)
  • default: var(--nz-text-disabled)
  • color on .nz-field-label
--nz-field-label-font--nz-font-label-m
  • font on .nz-field-label
--nz-field-required-fg--nz-text-critical
  • color on .nz-field-required

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

On this page