Validación de formularios con Vuelidate

Intermedio
Vuejs
Vuejs
Actualizado: 27/03/2026

Introducción a Vuelidate

Vuelidate es una librería de validación ligera y flexible para Vue.js que se integra de forma natural con la Composition API. A diferencia de otras soluciones, Vuelidate se basa en modelos y no en plantillas, lo que permite definir las reglas de validación directamente junto al estado reactivo del formulario.

Vuelidate proporciona validadores predefinidos, soporte para validaciones asíncronas, validación cruzada entre campos y un sistema de errores detallado que facilita mostrar mensajes específicos al usuario.

Instalación

Para utilizar Vuelidate es necesario instalar dos paquetes: el núcleo de la librería y la colección de validadores predefinidos:

npm install @vuelidate/core @vuelidate/validators

Configuración básica con useVuelidate

La función useVuelidate recibe dos argumentos: un objeto con las reglas de validación y un objeto con el estado reactivo a validar. Devuelve un objeto v$ que contiene toda la información de validación:

<template>
  <form @submit.prevent="enviar">
    <div>
      <label for="nombre">Nombre</label>
      <input id="nombre" v-model="formulario.nombre" type="text" />
      <span v-if="v$.nombre.$error" class="error">
        {{ v$.nombre.$errors[0].$message }}
      </span>
    </div>

    <div>
      <label for="email">Email</label>
      <input id="email" v-model="formulario.email" type="email" />
      <span v-if="v$.email.$error" class="error">
        {{ v$.email.$errors[0].$message }}
      </span>
    </div>

    <button type="submit">Enviar</button>
  </form>
</template>

<script setup lang="ts">
import { reactive } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import { required, email, minLength } from '@vuelidate/validators'

const formulario = reactive({
  nombre: '',
  email: ''
})

const reglas = {
  nombre: { required, minLength: minLength(3) },
  email: { required, email }
}

const v$ = useVuelidate(reglas, formulario)

async function enviar() {
  const esValido = await v$.value.$validate()
  if (!esValido) return
  console.log('Formulario válido:', { ...formulario })
}
</script>

Validadores predefinidos

Vuelidate incluye una amplia colección de validadores listos para usar. Los más habituales son:

| Validador | Descripción | Ejemplo | |---|---|---| | required | El campo no puede estar vacío | { required } | | email | Formato de email válido | { email } | | minLength(n) | Longitud mínima de caracteres | { minLength: minLength(5) } | | maxLength(n) | Longitud máxima de caracteres | { maxLength: maxLength(100) } | | numeric | Solo valores numéricos | { numeric } | | between(min, max) | Valor entre un mínimo y un máximo | { between: between(18, 120) } | | sameAs(ref) | Debe coincidir con otro campo | { sameAs: sameAs(password) } | | url | Formato de URL válido | { url } | | alpha | Solo caracteres alfabéticos | { alpha } |

Se pueden combinar múltiples validadores en un mismo campo para crear reglas más estrictas:

const reglas = {
  username: { required, minLength: minLength(3), maxLength: maxLength(20) },
  edad: { required, numeric, between: between(18, 120) },
  sitioWeb: { url }
}

Estado de validación

El objeto v$ expone propiedades reactivas que describen el estado de cada campo y del formulario completo:

  • $invalid: true si alguna regla no se cumple.
  • $dirty: true si el usuario ha interactuado con el campo (se ha hecho $touch).
  • $error: true cuando el campo es $invalid y $dirty al mismo tiempo. Es la propiedad más útil para mostrar errores.
  • $errors: array de objetos de error con $message, $property y $validator.
  • $pending: true si hay validaciones asíncronas en curso.
<template>
  <div>
    <input v-model="formulario.nombre" @blur="v$.nombre.$touch()" />
    <div v-if="v$.nombre.$error">
      <p v-for="error in v$.nombre.$errors" :key="error.$uid" class="error">
        {{ error.$message }}
      </p>
    </div>
    <p>Dirty: {{ v$.nombre.$dirty }}</p>
    <p>Invalid: {{ v$.nombre.$invalid }}</p>
  </div>
</template>

Disparar y resetear la validación

Vuelidate ofrece tres métodos para controlar cuando se ejecuta la validación:

$touch()

Marca un campo o el formulario completo como dirty, activando la visualización de errores. Se suele usar en el evento @blur:

<input v-model="formulario.email" @blur="v$.email.$touch()" />

$validate()

Ejecuta la validación de todos los campos y devuelve una promesa con un booleano. Marca automáticamente todos los campos como dirty:

async function enviar() {
  const esValido = await v$.value.$validate()
  if (esValido) {
    // procesar formulario
  }
}

$reset()

Restablece el estado de validación, poniendo $dirty a false en todos los campos. Útil cuando se quiere limpiar el formulario:

function resetearFormulario() {
  formulario.nombre = ''
  formulario.email = ''
  v$.value.$reset()
}

Mostrar mensajes de error

La forma más común de mostrar errores es iterar sobre el array $errors de cada campo. Cada error contiene un $message descriptivo:

<template>
  <div class="campo">
    <label for="password">Contraseña</label>
    <input
      id="password"
      v-model="formulario.password"
      type="password"
      :class="{ 'input-error': v$.password.$error }"
      @blur="v$.password.$touch()"
    />
    <ul v-if="v$.password.$error" class="lista-errores">
      <li v-for="error in v$.password.$errors" :key="error.$uid">
        {{ error.$message }}
      </li>
    </ul>
  </div>
</template>

Validadores personalizados

Validador síncrono

Un validador personalizado es simplemente una función que recibe el valor y devuelve true o false:

import { helpers } from '@vuelidate/validators'

const contieneNumero = helpers.withMessage(
  'Debe contener al menos un número',
  (value: string) => /\d/.test(value)
)

const contieneMayuscula = helpers.withMessage(
  'Debe contener al menos una letra mayúscula',
  (value: string) => /[A-Z]/.test(value)
)

const reglas = {
  password: { required, minLength: minLength(8), contieneNumero, contieneMayuscula }
}

La función helpers.withMessage permite asociar un mensaje de error personalizado al validador.

Validador asíncrono

Los validadores asíncronos son funciones que devuelven una promesa. Son útiles para comprobar datos contra un servidor, como verificar si un nombre de usuario está disponible:

import { helpers } from '@vuelidate/validators'

const usernameDisponible = helpers.withMessage(
  'Este nombre de usuario ya está en uso',
  helpers.withAsync(async (value: string) => {
    if (value === '') return true
    const response = await fetch(`/api/check-username?username=${value}`)
    const data = await response.json()
    return data.disponible
  })
)

const reglas = {
  username: { required, minLength: minLength(3), usernameDisponible }
}

Mientras la validación asíncrona está en curso, v$.username.$pending será true, lo que permite mostrar un indicador de carga.

Validación cruzada con sameAs

La validación cruzada entre campos se realiza con el validador sameAs. El caso más común es la confirmación de contraseña:

<script setup lang="ts">
import { reactive, computed } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import { required, minLength, sameAs } from '@vuelidate/validators'

const formulario = reactive({
  password: '',
  confirmPassword: ''
})

const reglas = {
  password: { required, minLength: minLength(8) },
  confirmPassword: {
    required,
    sameAs: sameAs(computed(() => formulario.password))
  }
}

const v$ = useVuelidate(reglas, formulario)
</script>

Es importante pasar un computed a sameAs para que la comparación sea reactiva y se actualice cuando el campo de referencia cambie.

Estado de validación a nivel de formulario

Antes de enviar un formulario, se comprueba si todo el formulario es válido accediendo al estado global de v$:

<template>
  <form @submit.prevent="enviar">
    <!-- campos del formulario -->

    <button type="submit" :disabled="v$.$invalid">
      Enviar
    </button>

    <p v-if="v$.$invalid && v$.$dirty" class="aviso">
      Corrige los errores antes de enviar
    </p>
  </form>
</template>

<script setup lang="ts">
async function enviar() {
  const esValido = await v$.value.$validate()
  if (!esValido) {
    console.log('Formulario inválido')
    return
  }
  console.log('Enviando datos...')
}
</script>

Estilización de estados de validación

Se pueden añadir clases CSS dinámicas basadas en el estado de validación para dar feedback visual al usuario:

<template>
  <div class="campo">
    <input
      v-model="formulario.email"
      :class="{
        'input-valido': v$.email.$dirty && !v$.email.$error,
        'input-error': v$.email.$error
      }"
      @blur="v$.email.$touch()"
    />
  </div>
</template>

<style scoped>
.input-error {
  border-color: #dc3545;
  background-color: #fff5f5;
}

.input-valido {
  border-color: #28a745;
  background-color: #f0fff0;
}

.error {
  color: #dc3545;
  font-size: 0.85rem;
  margin-top: 0.25rem;
}
</style>

Ejemplo completo: formulario de registro

A continuación se presenta un ejemplo que combina todos los conceptos anteriores en un formulario de registro completo:

<template>
  <form @submit.prevent="enviar" class="registro">
    <h2>Crear cuenta</h2>

    <div class="campo">
      <label for="nombre">Nombre</label>
      <input id="nombre" v-model="formulario.nombre" :class="{ 'input-error': v$.nombre.$error }" @blur="v$.nombre.$touch()" />
      <ul v-if="v$.nombre.$error">
        <li v-for="e in v$.nombre.$errors" :key="e.$uid">{{ e.$message }}</li>
      </ul>
    </div>

    <div class="campo">
      <label for="email">Email</label>
      <input id="email" v-model="formulario.email" type="email" :class="{ 'input-error': v$.email.$error }" @blur="v$.email.$touch()" />
      <ul v-if="v$.email.$error">
        <li v-for="e in v$.email.$errors" :key="e.$uid">{{ e.$message }}</li>
      </ul>
    </div>

    <div class="campo">
      <label for="password">Contraseña</label>
      <input id="password" v-model="formulario.password" type="password" :class="{ 'input-error': v$.password.$error }" @blur="v$.password.$touch()" />
      <ul v-if="v$.password.$error">
        <li v-for="e in v$.password.$errors" :key="e.$uid">{{ e.$message }}</li>
      </ul>
    </div>

    <div class="campo">
      <label for="confirm">Confirmar contraseña</label>
      <input id="confirm" v-model="formulario.confirmPassword" type="password" :class="{ 'input-error': v$.confirmPassword.$error }" @blur="v$.confirmPassword.$touch()" />
      <ul v-if="v$.confirmPassword.$error">
        <li v-for="e in v$.confirmPassword.$errors" :key="e.$uid">{{ e.$message }}</li>
      </ul>
    </div>

    <button type="submit" :disabled="v$.$invalid">Registrarse</button>
  </form>
</template>

<script setup lang="ts">
import { reactive, computed } from 'vue'
import { useVuelidate } from '@vuelidate/core'
import { required, email, minLength, sameAs, helpers } from '@vuelidate/validators'

const contieneNumero = helpers.withMessage(
  'Debe contener al menos un número',
  (value: string) => /\d/.test(value)
)

const contieneMayuscula = helpers.withMessage(
  'Debe contener al menos una mayúscula',
  (value: string) => /[A-Z]/.test(value)
)

const formulario = reactive({
  nombre: '',
  email: '',
  password: '',
  confirmPassword: ''
})

const reglas = {
  nombre: { required, minLength: minLength(3) },
  email: { required, email },
  password: { required, minLength: minLength(8), contieneNumero, contieneMayuscula },
  confirmPassword: { required, sameAs: sameAs(computed(() => formulario.password)) }
}

const v$ = useVuelidate(reglas, formulario)

async function enviar() {
  const esValido = await v$.value.$validate()
  if (!esValido) return
  console.log('Registro completado:', { ...formulario })
}
</script>

Este patrón cubre la gran mayoría de los casos de uso de validación de formularios en aplicaciones Vue.

Alan Sastre - Autor del tutorial

Alan Sastre

Ingeniero de Software y formador, CEO en CertiDevs

Ingeniero de software especializado en Full Stack y en Inteligencia Artificial. Como CEO de CertiDevs, Vuejs es una de sus áreas de expertise. Con más de 15 años programando, 6K seguidores en LinkedIn y experiencia como formador, Alan se dedica a crear contenido educativo de calidad para desarrolladores de todos los niveles.

Más tutoriales de Vuejs

Explora más contenido relacionado con Vuejs y continúa aprendiendo con nuestros tutoriales gratuitos.

Aprendizajes de esta lección

Cursos que incluyen esta lección

Esta lección forma parte de los siguientes cursos estructurados con rutas de aprendizaje