Conditional fields
Use a synchronous resolver when a UI property depends on form values or runtime context. A resolver returns the property value for the current form state.
Show or hide a field
Set visible to a resolver that returns a boolean:
{
kind: "field",
path: "companyName",
control: "text",
label: "Company name",
required: ({ accountType }) => accountType === "company",
visible: ({ accountType }) => accountType === "company",
props: {
placeholder: "Compiler Labs",
autoComplete: "organization",
},
},When accountType changes, "Form, Please" resolves the UI tree again. The
companyName field appears only for a company account.
visible has these effects:
- A hidden node does not render.
- A hidden section or array does not render its descendants.
- A hidden field keeps its value in React Hook Form state.
- The Standard Schema still receives the hidden value during submission.
visible does not clear a value or change schema validation.
Derive field properties
Field properties can use the same form values:
{
kind: "field",
path: "address",
control: "text",
label: ({ delivery }) => {
if (delivery === "shipping") return "Shipping address"
return "Address"
},
description: ({ locked }) => {
if (locked) return "Unlock the order to edit this address."
return "Enter the complete shipping address."
},
visible: ({ delivery }) => delivery === "shipping",
readOnly: ({ locked }) => locked,
required: ({ delivery }) => delivery === "shipping",
props: ({ delivery }) => {
if (delivery === "shipping") {
return { placeholder: "12 Analytical Engine Way" }
}
return {}
},
},On an ordinary definition node, the first resolver argument is the complete, deeply readonly schema input. A resolver authored inside a reusable fragment receives that fragment's local input instead. It is not only the value at the field path.
These field properties accept a static value or a synchronous resolver:
| Property | Result | Default |
|---|---|---|
visible | Shows or hides the field. | true |
disabled, readOnly | Changes the control interaction state. | false |
required | Passes required UI state to the control. | false |
label, description | Changes displayed content. | No content |
props, slotOptions | Changes control props or slot configuration. | Control-dependent |
Layout properties such as className and span also accept resolvers. See
Definitions for the complete property reference.
Ordinary resolvers must return immediately. A resolver that returns a promise causes a runtime error. Keep resolvers small and free of side effects. Do not fetch data or update form state in an ordinary resolver.
A selectable field's options property has a separate function contract. It
may return a promise and tracks the values and context properties it reads.
See Async options.
Apply one condition to a group
Put visible, disabled, or readOnly on a section or array when one condition
applies to all descendants:
const projectDefinition = nativeFormKit.defineForm(projectSchema, {
ui: [
{
kind: "section",
id: "project-details",
title: "Project details",
readOnly: ({ archived }) => archived,
children: [
{
kind: "field",
path: "details.name",
control: "text",
label: "Name",
},
{
kind: "field",
path: "details.summary",
control: "text",
label: "Summary",
},
],
},
],
})A descendant cannot override inherited state:
- A hidden parent hides all descendants.
- A disabled parent disables all descendants.
- A read-only parent makes all descendants read-only.
required is a field property. It is not inherited from a section or array.
Use runtime context
Use context for data that is not part of the editable form value. Examples include permissions, feature flags, and loaded option lists.
Bind the context type once with kit.forContext<Context>():
const editorDefinition = editorKit.defineForm(editorSchema, {
ui: [
{
kind: "field",
path: "country",
control: "select",
label: "Country",
disabled: (_values, { context }) => !context.canEdit,
options: ({ context }) => context.countries,
},
],
})
function Editor({ context }: { readonly context: EditorContext }) {
const form = editorKit.useForm(editorDefinition, {
defaultValues: { country: "" },
context,
})
return <editorKit.AutoForm form={form} />
}The second resolver argument contains the current, deeply readonly context.
Pass a concrete context value to useForm.
Keep requests and cache state in the application. Pass their current state
through context. See Resource state for fromResource examples.
Use resolvers inside arrays carefully
An array-child resolver receives the complete root input. It does not receive the current row or its index. Therefore, the same resolver result applies to each generated row for a given form state.
Use an array-node resolver for conditions that depend on the complete array:
const contactsDefinition = nativeFormKit.defineForm(contactsSchema, {
ui: [
{
kind: "array",
path: "contacts",
label: ({ contacts }) => `Contacts (${contacts.length})`,
itemDefault: { email: "" },
children: [
{
kind: "field",
path: "email",
control: "text",
label: "Email",
props: { type: "email" },
},
],
},
],
})Keep row and cross-row validation in the Standard Schema. Use custom array UI when the UI condition must depend on the current row index. See Arrays for custom operations and array slots.
Keep conditional validation in the schema
visible and required control the rendered UI. They do not add, remove, or
change schema rules.
Express the same condition in the Standard Schema:
const accountSchema = z
.object({
accountType: z.enum(["personal", "company"]),
companyName: z.string().optional(),
})
.superRefine((value, context) => {
if (
value.accountType === "company" &&
(value.companyName ?? "").trim().length === 0
) {
context.addIssue({
code: "custom",
message: "Company name is required",
path: ["companyName"],
})
}
})
.transform(({ companyName, ...account }) => {
if (account.accountType === "personal") return account
return { ...account, companyName: companyName?.trim() }
})This schema requires companyName only for a company account. Its transform
also removes a stale companyName from personal-account output.
Use a schema transform when cleanup is part of the submission contract. Use
value middleware when the source and cleared value must commit atomically. Use
form.update for the same managed boundary in an application action. See
Value middleware.