GitHub

Validation

Configure when validation runs, how it behaves, and how to apply errors from the server.

Triggers

Triggers determine which interaction causes a field to validate. Configure them once on the form and override per field where needed.

TriggerDefaultFires when
onBlurtrueField loses focus
onChangetrueValue is committed
onInputtrueEvery keystroke
onFocusfalseField gains focus
onMountfalseComponent 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' },
])
Server errors live in the same errors array as schema errors. When validateField runs again for a field, its server error is replaced by the schema result.