Getting started

vee-validate is built from the ground up with the composition API through a collection of functions, mainly the useField and useForm functions. Internally the <Form /> and <Field /> components actually use the composition functions under the hood.

Meaning you can create your own custom input and form components and they will be treated the same as <Form /> and <Field /> components. You can mix them together and use a Form component with any other custom component that uses useField and vice versa.

vee-validate supports synchronous and asynchronous validation and allows defining rules on the field level or the form level using validation schemas. vee-validate has first-class support for:

  • yup through @vee-validate/yup package.
  • zod through @vee-validate/zod package.
  • valibot through @vee-validate/valibot package.
  • Global validators (Laravel-like syntax) through @vee-validate/rules.

vee-validate has historically been a declarative validation library, and while the composition API changes things a bit, it still follows the same mindset of declarative validation. vee-validate optimizes for building fields and forms, not values.

When to use composition API

While vee-validate offers both declarative components and composition functions to supercharge your forms, it is always up to you to decide which one to use. However, the composition API is easier to integrate and more flexible. You can build custom components with it or integrate it with any UI library. It is generally recommended to use the composition API.

On this page, you will learn how to declare forms and how to hook your elements and components into vee-validate forms and achieve value tracking, validation, and more.

Declaring Forms

Form context

You can declare forms with the useForm function exported from the vee-validate core package. This is a composition API function that marks the current component as a form.

vue<script setup>
import { useForm } from 'vee-validate';

// Creates a form context
// This component now acts as a form
// Usually you will destruct the form context to get what you need
const { values } = useForm();

  <pre>{{ values }}</pre>

Calling useForm creates a form context in the component and provides it for any child component that injects it. This means you should stick to calling useForm once in a component.

Creating a form context does a few things:

  • Acts as a value collector for all the fields you will declare as child components.
  • Validates the fields and aggregates the errors.
  • Aggregates the validity, touched, and dirty states of all the fields.

Field binds

With useForm declared, you are now ready to integrate the form with your elements and components. vee-validate is agnostic to the UI you are using.

You will learn how to associate your components and elements with the form and how to get value collection, validation, and error messages working.

HTML Inputs

useForm provides a function called defineField. This function accepts a field path and returns a value model and an object containing the bindings for the input element. The field path is a string that represents the path to the field in the form context.

For example, if you have a field called email in the form context, the field path will be email.

tsconst { defineField } = useForm();

const [email, emailAttrs] = defineField('email');

Note that defineField generates a pair of values, the first one is the value model and the second one is the attributes/props object. The props object contain attributes or event listeners that are useful to have on the input control or component which enables custom validation triggers and more.

Here is a basic example of how to use defineField with a simple input element:

Notice that as you type in the input, the values are automatically updated with the value changes.

Let’s quickly add a validation schema on the form to see some errors on the form. We will be using yup throughout the examples, but you can use zod or any other supported validation library you want.

To add a yup schema or any kind of form schema, you pass it to the validationSchema option when calling useForm. Naturally, form schemas are almost always an object or a shape schema.

tsimport * as yup from 'yup';

const schema = yup.object({
  email: yup.string().required().email(),

const { defineField } = useForm({
  validationSchema: schema,

const [email, emailAttrs] = defineField('email');

Here is a full running example:

Notice as you type into the input, the validation is then triggered and the errors are populated. By default defineField optimizes for aggressive validation, meaning the validation will be triggered whenever the model changes.

You can change that behavior. For example, you can make it “lazy” by passing a configuration to defineField to disable validation on model updates with validateOnModelUpdate config.

Now as you type in the field, the input is not immediately validated. You can do more with dynamic configurations.


Similarly to HTML inputs, you can achieve the same results with the same defineField function.

Following the previous examples we can achieve value tracking like this:

As you type into the input, notice that the values are being updated.

Let’s add validation to the previous example:

Notice that for components, validations are executed immediately. This is intended because component implementations are not standardized across the Vue ecosystem as there is no guarantee it will emit the same range of events as the native HTML elements. However, you can customize the validation trigger if you know the components you are using are emitting the right events to support the behavior.

Mapping attributes and props

But aside from the attributes and listeners that vee-validate adds to those binding objects, you can also map the attributes and props of the in. This is useful when you want to:

  • When you want to map the attributes/props to a different name.
  • Pass new attributes/props to the component/element based on the field state.

You can use props to include any additional props or attributes you need to add to the component/element.

tsconst { defineField } = useForm({
  // ...

const [email, emailProps] = defineField('email', { props: state => ({ error: state.errors[0] }) });

In the following example, we have a component that accepts an error string prop and shows that message if it is not empty. This is common in many UI libraries as they try not to lock you into a specific validation library.


The state object contains a lot of useful information about the field, it is fully typed so you can explore it with your IDE or visit the source reference for more information.

Notice that form also gives you access to errors so you can reference them anywhere in the component.

Dynamic configuration

Instead of passing a static configuration object defineField, you could pass a function that returns different configuration values. This is useful when you want the configuration to be dynamic based on the field state.

Here is an example that shows how to make the validation behavior “eager”. Meaning if the field does not have any errors then it will only validate on change.

But once it is invalid, it validates on each input event, making it “eager” for success.

Form Schema

As you have seen in the previous examples, the useForm function accepts a validationSchema that is used to validate the form. We’ve been using yup to define the schema however you can use zod or any 3rd-party validators.

Validating with Yup

You can pass yup schemas directly as you’ve seen previously, but it is recommended that you use the @vee-validate/yup package. It will enable better support of yup features and unlock full typescript support for your schemas.

sh# with npm
npm i @vee-validate/yup
# with pnpm
pnpm add @vee-validate/yup
# with yarn
yarn add @vee-validate/yup

The package exposes a toTypedSchema function that you can use to wrap your yup schemas, this allows vee-validate to infer form input and output types. More on that here.

tsimport { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/yup';
import * as yup from 'yup';

// Creates a typed schema for vee-validate
const schema = toTypedSchema(
    email: yup.string().required().email(),

const { errors, values } = useForm({
  validationSchema: schema,

Yup Schema Optimizations

There are a couple of optimization caveats when it comes to using yup schemas to validate your forms, be sure to check the best practices guide.

Validating with Zod

You can use Zod in a very similar manner to how we’ve been using yup in the past examples. However, you will need to add @vee-validate/zod to your dependencies.

sh# with npm
npm i @vee-validate/zod
# with pnpm
pnpm add @vee-validate/zod
# with yarn
yarn add @vee-validate/zod

Then you can wrap your zod schemas with toTypedSchema function which allows vee-validate to infer form input and output types. More on that here.

tsimport { useForm } from 'vee-validate';
import { toTypedSchema } from '@vee-validate/zod';
import { z } from 'zod';

// Creates a typed schema for vee-validate
const schema = toTypedSchema(
    email: z.string().nonempty().email(),

const { errors, values } = useForm({
  validationSchema: schema,

Here is a full example using zod with useForm:


There is a known issue with Zod’s refine and superRefine not executing whenever some object keys are missing which is common with forms. This is not an issue with vee-validate as it is a design choice in Zod at the moment. Refer to this issue for explanations and further reading.

Validating with Valibot

You can also use Valibot which is is a schema library with bundle size, type safety and developer experience in mind. It is a great alternative to Yup and Zod if bundle size is a concern. You will need to use the @vee-validate/valibot package to use valibot schemas.

sh# with npm
npm i @vee-validate/valibot
# with pnpm
pnpm add @vee-validate/valibot
# with yarn
yarn add @vee-validate/valibot

Then you can wrap your Valibot schemas with toTypedSchema function which allows vee-validate to infer form input and output types. More on that here.

tsimport { useForm } from 'vee-validate';
import * as v from 'valibot';
import { toTypedSchema } from '@vee-validate/valibot';

// Creates a typed schema for vee-validate
const schema = toTypedSchema(
    email: v.pipe(v.string(),'Invalid email'), v.nonEmpty('required')),

const { errors, values } = useForm({
  validationSchema: schema,

Here is a full example using valibot with useForm:

Other validation providers and options

Global Rules

Another option is using @vee-validate/rules which have been historically bundled with past versions of vee-validate. It includes rules that can be defined globally and then used anywhere using Laravel-like string expressions.

You can refer to the full guide on global rules here.

Validating with functions

Another option is to just use any 3rd party validation tool you prefer, something like validator.js. Here is a quick example:

Or you could use any custom function.


Both zod and yup are very good at defining schemas especially nested values so it is recommended that you use either. As an added bonus, you get full typescript support with either of them. You can refer to the Typed schemas guide to learn more about how to maximize the type safety of your schemas.

Dynamic Form Schemas

There are a few ways you can create dynamic schemas (reactive) where it changes the validation rules based on some state. The first way to do that is with computed.

When the validation schema value changes, only the fields that were validated at least once will be re-validated, the other fields won’t be validated to avoid aggressive validation behavior.

There are other ways depending on which validation library you are using. For example, with yup you can achieve the same with yup.lazy or zod.lazy:

Next Step

Now that you've learned how to use forms to define fields, collect their values and validate them. Next, you will learn how to handle submissions and implement advanced patterns with forms.

Handling formsSubmissions, resets and form state