Basic Form Validation

Compute Errors From Refs

Basic Form Validation

For simple forms a computed errors object and conditional rendering beat reaching for a library. No dependencies, full type inference.

4 min read Level 2/5 #vue#forms#validation
What you'll learn
  • Build an errors computed
  • Show errors conditionally in the template
  • Disable submit when invalid

For small forms (login, contact, settings) a computed errors object covers everything you need. Use a library when complexity outgrows it.

Computed Errors

<script setup lang="ts">
import { ref, computed } from 'vue'

const email = ref('')
const password = ref('')

const errors = computed(() => ({
  email: !email.value.includes('@') ? 'Enter a valid email' : null,
  password: password.value.length < 8 ? 'At least 8 characters' : null,
}))

const isValid = computed(() =>
  Object.values(errors.value).every(e => e === null)
)
</script>

Rendering Errors

<template>
  <label>
    Email
    <input v-model.trim="email" type="email" />
    <p v-if="errors.email" class="error">{{ errors.email }}</p>
  </label>

  <label>
    Password
    <input v-model="password" type="password" />
    <p v-if="errors.password" class="error">{{ errors.password }}</p>
  </label>

  <button :disabled="!isValid">Sign in</button>
</template>

Show Errors Only After Touch

Showing errors before the user has typed anything is annoying. Track a touched ref per field:

<script setup lang="ts">
import { ref, reactive, computed } from 'vue'

const email = ref('')
const touched = reactive({ email: false })

const visibleEmailError = computed(() =>
  touched.email && !email.value.includes('@') ? 'Invalid email' : null
)
</script>

<template>
  <input v-model="email" @blur="touched.email = true" />
  <p v-if="visibleEmailError">{{ visibleEmailError }}</p>
</template>

When forms grow more than a handful of fields, move on to VeeValidate or a Zod-driven helper instead of hand-rolling everything.

VeeValidate — Full Form Library →