Vue 3+VeeValidate – Form Validation Example (Composition API)

Example 1: Vue 3+VeeValidate – Form Validation Example (Composition API)

Vue Composition API

The component in the example is built with the new Vue Composition API that comes with Vue 3, component logic is located within a <script setup> block that shares the same scope as the template, so variables and functions declared in the block are directly accessible to the template. For more info on the Vue Composition API see


VeeValidate is a library for building, validating and handling forms in Vue.js. VeeValidate 4 was recently released and is compatible with Vue 3, the official docs are available at

The example is a simple registration form with pretty standard fields for title, first name, last name, date of birth, email, password, confirm password and an accept terms and conditions checkbox. All fields are required including the checkbox, the dob must be a valid date, the email address must be in a valid format, the password field must have a min length of 6, and the confirm password and password fields must match.

Styling of the example is all done with Bootstrap 4.5 CSS, for more info see

Here it is in action:

Vue 3 + VeeValidate Form Validation App Component with Composition API

The app component code defines the form validation rules with the Yup schema validation library which VeeValidate supports out of the box, for more info on Yup see

The onSubmit() method is called when the form is valid and submitted, and simply displays the contents of the form in a javascript alert.

The app component template contains the form with all of the input fields and validation messages. The form and fields are built with the VeeValidate <Form /> and <Field /> components which automatically hook into the validation rules (schema) based on the name of the field.

The form calls the onSubmit() method when the form is submitted and valid. Validation rules are bound to the form with the validation-schema prop, and validation errors are provided to the form template via the scoped slot v-slot="{ errors }".

<script setup>
import { Form, Field } from 'vee-validate';
import * as Yup from 'yup';

const schema = Yup.object().shape({
    title: Yup.string()
        .required('Title is required'),
    firstName: Yup.string()
        .required('First Name is required'),
    lastName: Yup.string()
        .required('Last name is required'),
    dob: Yup.string()
        .required('Date of Birth is required')
        .matches(/^\d{4}-(0[1-9]|1[012])-(0[1-9]|[12][0-9]|3[01])$/, 'Date of Birth must be a valid date in the format YYYY-MM-DD'),
    email: Yup.string()
        .required('Email is required')
        .email('Email is invalid'),
    password: Yup.string()
        .min(6, 'Password must be at least 6 characters')
        .required('Password is required'),
    confirmPassword: Yup.string()
        .oneOf([Yup.ref('password'), null], 'Passwords must match')
        .required('Confirm Password is required'),
    acceptTerms: Yup.string()
        .required('Accept Ts & Cs is required')

function onSubmit(values) {
    // display form values on success
    alert('SUCCESS!! :-)\n\n' + JSON.stringify(values, null, 4));

    <div class="card m-3">
        <h5 class="card-header">Vue 3 + VeeValidate - Form Validation Example</h5>
        <div class="card-body">
            <Form @submit="onSubmit" :validation-schema="schema" v-slot="{ errors }">
                <div class="form-row">
                    <div class="form-group col">
                        <Field name="title" as="select" class="form-control" :class="{ 'is-invalid': errors.title }">
                            <option value=""></option>
                            <option value="Mr">Mr</option>
                            <option value="Mrs">Mrs</option>
                            <option value="Miss">Miss</option>
                            <option value="Ms">Ms</option>
                        <div class="invalid-feedback">{{errors.title}}</div>
                    <div class="form-group col-5">
                        <label>First Name</label>
                        <Field name="firstName" type="text" class="form-control" :class="{ 'is-invalid': errors.firstName }" />
                        <div class="invalid-feedback">{{errors.firstName}}</div>
                    <div class="form-group col-5">
                        <label>Last Name</label>
                        <Field name="lastName" type="text" class="form-control" :class="{ 'is-invalid': errors.lastName }" />
                        <div class="invalid-feedback">{{errors.lastName}}</div>
                <div class="form-row">
                    <div class="form-group col">
                        <label>Date of Birth</label>
                        <Field name="dob" type="date" class="form-control" :class="{ 'is-invalid': errors.dob }" />
                        <div class="invalid-feedback">{{errors.dob}}</div>
                    <div class="form-group col">
                        <Field name="email" type="text" class="form-control" :class="{ 'is-invalid': }" />
                        <div class="invalid-feedback">{{}}</div>
                <div class="form-row">
                    <div class="form-group col">
                        <Field name="password" type="password" class="form-control" :class="{ 'is-invalid': errors.password }" />
                        <div class="invalid-feedback">{{errors.password}}</div>
                    <div class="form-group col">
                        <label>Confirm Password</label>
                        <Field name="confirmPassword" type="password" class="form-control" :class="{ 'is-invalid': errors.confirmPassword }" />
                        <div class="invalid-feedback">{{errors.confirmPassword}}</div>
                <div class="form-group form-check">
                    <Field name="acceptTerms" type="checkbox" id="acceptTerms" value="true" class="form-check-input" :class="{ 'is-invalid': errors.acceptTerms }" />
                    <label for="acceptTerms" class="form-check-label">Accept Terms & Conditions</label>
                    <div class="invalid-feedback">{{errors.acceptTerms}}</div>
                <div class="form-group">
                    <button type="submit" class="btn btn-primary mr-1">Register</button>
                    <button type="reset" class="btn btn-secondary">Reset</button>

Example 2: Vue 3+VeeValidate – Form Validation Example (Composition API)

While coding an address create component, I needed a form validation library and I decided to use the latest version of the vee-validate library. I made this component using Vue2 before, so I knew what I needed to do and how. Since I wanted to write my code as reusable with custom components, I had to read a lot of documentation. In this article, I will share the specific problems I encountered and the solutions I developed step by step.

Note: While writing this article, I tried to write as simple as possible in order not to include repetitive code and not to complicate the component. I think that with this article, you will be able to easily make a component like the one below.

Vue 3+VeeValidate - Form Validation Example (Composition API)


First of all, because I will use custom components, I proceeded through the ‘composition api’ section instead of the ‘components’ section in the vee validate documentation.

FormText Component

<input v-model="value" type="text" :name="name"/>
</template><script setup>
import { defineProps } from "vue";
import { useField } from "vee-validate";const props = defineProps({
name: { type: String, required: true },
});const { value, errorMessage } = useField(;</script>

If you noticed this component, it was written to be used in form validation, so there is no need to pass input’s current values to parent component. Every change made on the input is automatically assigned to the form object that we will define on the parent. The useField hook here helps us to get and set the values ​​of the field we defined on the form by passing fields name.

AddressForm Component

I prefer the yup package to handle form validations with vee-validate. With yup, we can easily define nested form objects with a cleaner structure. I will not go into the details of the yup, the documentation is written very well, you can read it here.

There are two businesses on that component.

First Case:

  • If address type is ‘personal’, user enters identityNumber,
  • else user enters companyName

Second Case:

  • User selects country
  • Cities in selected country is fetched from backend
  • User selects city in selected country

First case is easily solved with yup using ‘when’ method but I wasted so much time for handling second case. Remember I used lots of different approaches to handle those problems but using Vee Validate with Composition API was the best solution for me.

Solution: Second case

  • useForm hook used for creating form inside script setup.
  • Yup schema and initialValues was passed to useForm hook and setFieldValue and handleSubmit methods returned from that hook.
  • useFieldValue hook is used for getting current countryId
  • I used watch to handle countryId changes. When countryId is changed cities in selected country is fetched.
  • If country is changed after selecting a city, city field should set null. I used setFieldValue here.
<FormText name="address.addressTitle"/>
<FormSelect name="address.countryId" :items="countries"/>
<FormSelect name="address.cityId" :items="cities"/> <FormRadioGroup
:items="ADDRESS_TYPES" name="address.addressType"/> <FormText v-show="addressType === 'personal'" name="address.identityNumber" />
<FormText v-show="addressType === 'company'" name="address.companyName" /> <button @click="submit">Add Address</button>
</template><script setup>
import * as yup from "yup";
import { useForm, useFieldValue } from "vee-validate";
import { reactive, onMounted, watch } from "vue";const schema = yup.object({
address: yup.object({
addressTitle: yup.string().required(),
countryId: yup.number().required(),
cityId: yup.number().required(),
addressType: yup.string().required(),
identityNumber: yup.string().when("addressType", {
is: 'personal',
then: yup.string().required(),
companyName: yup.string().when("addressType", {
is: 'company',
then: yup.string().required(),
});const { setFieldValue, handleSubmit } = useForm({
validationSchema: schema,
initialValues: {
address: { addressType: "personal" },
});const countryId = useFieldValue("address.countryId");
const addressType = useFieldValue("address.addressType");watch(() => countryId.value, (newValue) => {
if (newValue) {
await store.fetchCities(newValue);
cityId.value && setFieldValue("address.cityId", null);
});const failValidation = ({ values, errors, results }) => {
}const addAddress = handleSubmit((values) => {
//if validates, this code will work; else executes failValidation
}, failValidation);

You can also customize vee-validate errors using vue-i18n and yup.

import { useI18n } from "vue-i18n";
import * as yup from "yup";const { t } = useI18n();yup.setLocale({
   mixed: {
      required: t("required"),

More Error Tutorials >>

Vue 3+VeeValidate – Form Validation Example (Composition API)

Vue 3+VeeValidate – Required Checkbox Example (Composition API)

Leave a Reply

Your email address will not be published.