Conditional Validation in VeeValidate Vue 3 Without Template Sprawl
Keep conditional validation out of Vue templates. Use VeeValidate v4 useForm and useField with reactive meta and schema context to centralize conditional rules and cross-field validation in Vue 3 forms.
10 Nov 2025, 19:12 UTC

Complex forms break when validation rules leak into templates. A shipping address that is only required when "ship different" is checked ends up with v-if guards, manual watchers, and duplicated error messages. The useful takeaway is to keep the condition in validation logic and let VeeValidate's reactive meta drive the UI.
This works in VeeValidate v4+ for Vue 3. The composition API centers on useForm for form-level state and useField for per-field state. Both expose reactive refs: value, errorMessage, and meta. Meta contains touched, dirty, pending, valid and other flags without manual watchers.
Form state stays centralized with useForm
useForm returns a handleSubmit wrapper and aggregated meta. isSubmitting, isValid and meta.valid reflect the combined state of all fields. That lets a submit button bind to isSubmitting and an error summary bind to meta.valid without scanning individual fields.
Validation can be declared via rule strings, rule objects, or an external schema such as Zod or Yup. The schema is external to VeeValidate, so behavior depends on the chosen schema library and its version.
Field reactivity with useField and meta
useField creates a field instance with value, errorMessage and meta. The component template binds to these refs directly. Because meta is reactive, UI feedback for touched, dirty and pending updates automatically on blur, change or submit depending on configuration.
Field-level validation supports conditional rules and cross-field dependencies through schema context and custom rules. For cross-field checks, the schema receives the whole form values, which avoids prop drilling in the template.
Worked example: reusable password composable with confirm
The pattern below shows a composable that encapsulates validation for password and confirmPassword, with a conditional requirement when the user opts into account creation.
import { useForm, useField } from 'vee-validate'
import { toTypedSchema } from '@vee-validate/zod'
import { z } from 'zod'
export function useAccountForm() {
const schema = toTypedSchema(
z.object({
createAccount: z.boolean(),
password: z.string().optional(),
confirmPassword: z.string().optional()
}).refine(data => {
if (!data.createAccount) return true
return data.password === data.confirmPassword
}, {
message: 'Passwords must match',
path: ['confirmPassword']
}).refine(data => {
if (!data.createAccount) return true
return (data.password?.length ?? 0) >= 8
}, {
message: 'Password must be at least 8 characters',
path: ['password']
})
)
const { handleSubmit, meta, isSubmitting } = useForm({
validationSchema: schema,
initialValues: { createAccount: false }
})
const createAccount = useField('createAccount')
const password = useField('password', undefined, {
validateOnBlur: true,
validateOnChange: true
})
const confirmPassword = useField('confirmPassword', undefined, {
validateOnBlur: true
})
const onSubmit = handleSubmit(values => {
// submit logic
})
return { createAccount, password, confirmPassword, handleSubmit: onSubmit, meta, isSubmitting }
}
Template usage binds to the refs returned by useField. Error messages render from field.errorMessage and visibility can be driven by field.meta.touched. The conditional requirement lives in the schema refine, not in v-if chains.
Trade-off: async validation and pending state
Async validators are supported, but they can introduce race conditions if the user types quickly. VeeValidate exposes meta.pending for each field. UI should disable or show a pending indicator while pending is true to avoid showing stale errors from an earlier request.
Schema libraries are external dependencies. Changing a schema library or its version changes error message shapes and validation timing. Keep validation definitions co-located with the form composable and avoid mixing rule strings with schema objects in the same form.
Limitations to check in your app: confirm that form.meta and field.meta update reactively on input by logging them in a minimal Vue 3 app with useForm and useField imported from vee-validate. Compare string rule syntax versus schema object to observe differences in error propagation and timing. Test an async validator with artificial delay and rapid input changes to verify pending handling and final error resolution.
Actionable check: move one conditional rule out of the template into schema context, expose meta.touched for error display, and bind submit to meta.valid and isSubmitting. Keep presentation thin and validation declarative.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.