Cross-field validation
Some rules involve more than one field: a confirmation must match, an end date must follow a start date. NotForm validates with your schema, so you write the rule in the schema and tell it which field should display the error.
Put the rule on the schema, and set the path
An issue is shown by the field whose path matches the issue's path. Report the issue on the field you want to display it.
import { z } from 'zod'
const schema = z.object({
confirmPassword: z.string().min(1, 'Confirm your password.'),
password: z.string().min(8, 'Use at least 8 characters.'),
}).refine((values) => {
return values.password === values.confirmPassword
}, {
message: 'Passwords do not match.',
path: ['confirmPassword'],
})
Without path, the issue is form-level and no field displays it.
Keep the dependent field up to date
Fields validate on their own events. If the user fixes password after confirmPassword showed an error, confirmPassword does not know to re-validate. Watch the source field and re-validate the dependent one.
<script setup lang="ts">
import { watch } from 'vue'
const password = form.field('password')
const confirmPassword = form.field('confirmPassword')
watch(
() => {
return password.model
},
() => {
// only after the user has interacted with the confirm field
if (confirmPassword.isTouched) {
void confirmPassword.validate()
}
}
)
</script>
<template>
<Form
:form="form"
novalidate
>
<Field
v-slot="{ field }"
:field="password"
>
<label :for="field.path">Password</label>
<input
:id="field.path"
v-model="field.model"
v-bind="field.events"
type="password"
>
<FieldError :errors="field" />
</Field>
<Field
v-slot="{ field }"
:field="confirmPassword"
>
<label :for="field.path">Confirm password</label>
<input
:id="field.path"
v-model="field.model"
v-bind="field.events"
type="password"
>
<FieldError :errors="field" />
</Field>
</Form>
</template>
Passing the pre-created field instances to Field is fine. Field accepts any field instance.
Date ranges
The same pattern applies to any dependent pair.
const schema = z.object({
endDate: z.string().min(1, 'Choose an end date.'),
startDate: z.string().min(1, 'Choose a start date.'),
}).refine((values) => {
return values.endDate >= values.startDate
}, {
message: 'End date must be on or after the start date.',
path: ['endDate'],
})
ISO date strings (2026-11-02) compare correctly as text. Watch startDate and re-validate endDate, exactly as above.
Show a rule for the whole form
For a rule that is not about one field, omit path and show it as a form-level error. See Errors.