On this page
Forms and v-model
v-model creates two-way binding between form inputs and reactive state. When the user types, state updates; when state changes, the input reflects it.
Basic Text Input
<script setup>
import { ref } from 'vue';
const username = ref('');
const email = ref('');
</script>
<template>
<form @submit.prevent="handleSubmit">
<label>
Username
<input v-model="username" type="text" />
</label>
<label>
Email
<input v-model="email" type="email" />
</label>
<button type="submit">Submit</button>
</form>
<p>Preview: {{ username }} — {{ email }}</p>
</template>
Input Types
v-model works with different input types:
<script setup>
import { ref, reactive } from 'vue';
const form = reactive({
message: '',
agree: false,
plan: 'free',
colors: [],
volume: 50,
});
</script>
<template>
<!-- Textarea -->
<textarea v-model="form.message" rows="3" />
<!-- Checkbox -->
<input v-model="form.agree" type="checkbox" /> I agree
<!-- Radio -->
<input v-model="form.plan" type="radio" value="free" /> Free
<input v-model="form.plan" type="radio" value="pro" /> Pro
<!-- Checkbox group -->
<input v-model="form.colors" type="checkbox" value="red" /> Red
<input v-model="form.colors" type="checkbox" value="blue" /> Blue
<!-- Range -->
<input v-model="form.volume" type="range" min="0" max="100" />
</template>
Select Dropdown
<script setup>
import { ref } from 'vue';
const country = ref('');
const countries = ['USA', 'Canada', 'UK', 'Australia'];
</script>
<template>
<select v-model="country">
<option disabled value="">Select a country</option>
<option v-for="c in countries" :key="c" :value="c">{{ c }}</option>
</select>
</template>
Modifiers
Append modifiers to v-model for common transformations:
<template>
<!-- Trim whitespace on input -->
<input v-model.trim="search" />
<!-- Cast to number -->
<input v-model.number="age" type="number" />
<!-- Update on change event instead of input -->
<input v-model.lazy="description" />
</template>
Basic Validation
<script setup>
import { ref, computed } from 'vue';
const email = ref('');
const password = ref('');
const emailError = computed(() => {
if (!email.value) return 'Email is required';
if (!email.value.includes('@')) return 'Invalid email';
return '';
});
const isValid = computed(() => !emailError.value && password.value.length >= 8);
function handleSubmit() {
if (!isValid.value) return;
console.log('Submitting', { email: email.value });
}
</script>
<template>
<form @submit.prevent="handleSubmit">
<input v-model="email" type="email" />
<p v-if="emailError" class="error">{{ emailError }}</p>
<input v-model="password" type="password" />
<button :disabled="!isValid">Sign Up</button>
</form>
</template>
For complex forms, consider VeeValidate or FormKit. Next: add client-side routing with Vue Router.