v-model
v-model is two-way binding sugar. On native inputs it reads value + listens for input; on components it reads the modelValue prop + emits update:modelValue.
Native, modifiers, custom components, multi
EXAMPLE
<script setup>
import { ref, computed } from 'vue';
const email = ref('');
const age = ref(0);
const checked = ref(false);
const color = ref('blue');
const tags = ref(['a', 'b']);
const comment = ref('');
</script>
<template>
<!-- 1) Native inputs — v-model uses value + input -->
<input v-model="email" type="email" placeholder="Email" />
<input v-model.number="age" type="number" />
<input v-model.trim="comment" />
<input v-model.lazy="comment" /> <!-- updates on change instead of input -->
<!-- 2) Checkbox — boolean OR array -->
<input type="checkbox" v-model="checked" /> {{ checked }}
<!-- Multiple checkboxes binding to an array -->
<input type="checkbox" v-model="tags" value="a" /> A
<input type="checkbox" v-model="tags" value="b" /> B
<input type="checkbox" v-model="tags" value="c" /> C
<p>{{ tags }}</p>
<!-- 3) Radio -->
<input type="radio" v-model="color" value="red" /> Red
<input type="radio" v-model="color" value="blue" /> Blue
<input type="radio" v-model="color" value="green" /> Green
<!-- 4) Select -->
<select v-model="color">
<option value="red">Red</option>
<option value="blue">Blue</option>
<option value="green">Green</option>
</select>
<!-- Multi-select -->
<select v-model="tags" multiple>
<option value="a">A</option>
<option value="b">B</option>
<option value="c">C</option>
</select>
<!-- 5) Textarea -->
<textarea v-model="comment" rows="4"></textarea>
</template>
<!-- 6) Custom component as a v-model target — needs modelValue prop + update:modelValue event -->
<!-- MoneyInput.vue -->
<script setup>
import { computed } from 'vue';
const props = defineProps({ modelValue: { type: Number, default: 0 } });
const emit = defineEmits(['update:modelValue']);
const display = computed({
get: () => props.modelValue.toFixed(2),
set: (v) => emit('update:modelValue', parseFloat(v) || 0),
});
</script>
<template>
<span>$</span>
<input :value="display" @@input="display = $event.target.value" inputmode="decimal" />
</template>
<!-- Usage -->
<MoneyInput v-model="price" />
<!-- 7) Multiple v-models — named -->
<!-- AddressInput.vue -->
<script setup>
defineProps(['street', 'city', 'postcode']);
defineEmits(['update:street', 'update:city', 'update:postcode']);
</script>
<template>
<input :value="street" @@input="$emit('update:street', $event.target.value)" placeholder="Street" />
<input :value="city" @@input="$emit('update:city', $event.target.value)" placeholder="City" />
<input :value="postcode" @@input="$emit('update:postcode', $event.target.value)" placeholder="Postcode" />
</template>
<!-- Parent -->
<AddressInput v-model:street="a.street" v-model:city="a.city" v-model:postcode="a.postcode" />
<!-- 8) Custom modifier — handle .trim / .number / your own -->
<script setup>
const props = defineProps({
modelValue: String,
modelModifiers: { default: () => ({}) },
});
const emit = defineEmits(['update:modelValue']);
function emitVal(value) {
if (props.modelModifiers.uppercase) value = value.toUpperCase();
emit('update:modelValue', value);
}
</script>
<!-- Parent -->
<UppercaseInput v-model.uppercase="name" />
<!-- 9) Best practices -->
<!-- - Always validate at the destination (form submit), not just via the binding -->
<!-- - For complex forms, reach for vee-validate / formkit instead of hand-rolled v-model -->
<!-- - Don't fight v-model — if you need extra logic, use :value / @input directly -->
Why it matters
v-model is just :modelValue + @update:modelValue with a sweet syntax. Knowing the underlying contract makes custom inputs (and multi-v-model components) trivial.
Tip: Tweak the snippet with Try it Yourself », then sit the quiz at the bottom of the page.
Example
Exercise
Two-way bind an input to data.
<input
"name">
Hyphenated directive.
Discussion
Loading…