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.