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.
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 →