Validation
Triggers
Triggers determine which interaction causes a field to validate. Configure them once on the form and override per field where needed.
| Trigger | Default | Fires when |
|---|---|---|
onBlur | true | Field loses focus |
onChange | true | Value is committed |
onInput | true | Every keystroke |
onFocus | false | Field gains focus |
onMount | false | Component mounts |
Per-field overrides
validateOn on <NotField> merges over the form config. Only the keys you specify are changed.
<template>
<!-- Form has onInput: false — this field overrides it -->
<NotField path="username" :validateOn="{ onInput: true }" v-slot="{ events }">
<input v-model="form.values.username" v-bind="events" />
</NotField>
</template>
Modes
The mode controls re-validation behaviour after an error has first appeared. Type into each field, blur it, then keep typing to see the difference.
Eager
Lazy
eager — validates on blur, then re-validates on every input/change while an error exists. Use for short forms where instant feedback matters.
lazy — validates only on blur or submit. No re-validation while typing. Use for long forms where constant feedback would feel intrusive.
Field vs form validation
validateField
Runs the full schema, then updates errors only for the targeted path. All other field errors are left untouched.
await form.validateField('email')
// Only errors for 'email' change
validate
Runs the schema and replaces all errors with the result.
const result = await form.validate()
if (result.issues) {
// Validation failed — result.issues contains all errors
} else {
console.log(result.value) // Typed, transformed schema output
}
Async validation
All schema validation is awaited. Async rules in your schema — HTTP calls, database lookups — work without any extra configuration.
<NotField path="username" v-slot="{ events, isValidating }">
<input v-model="form.values.username" v-bind="events" />
<span v-if="isValidating">Checking availability…</span>
</NotField>
Cross-field validation
Cross-field rules belong in the schema. NotForm runs the full schema on every validation call, so refine and superRefine work correctly.
const schema = z.object({
password: z.string().min(8),
confirm: z.string(),
}).refine(value => value.password === v.confirm, {
message: 'Passwords do not match',
path: ['confirm'],
})
When the user blurs confirm, validateField('confirm') surfaces the cross-field error at that path. Other field errors are not touched.
Server-side errors
Submit the form below with taken@example.com to see a server error applied to a specific field after submission.
setError replaces an existing error for the same path, or appends it if none exists. To bulk-replace all errors at once (e.g., from a 422 response):
form.setErrors([
{ path: [{ key: 'email' }], message: 'Already taken' },
{ path: [{ key: 'username' }], message: 'Too short' },
])
errors array as schema errors. When validateField runs again for a field, its server error is replaced by the schema result.