v-model on an Input
v-model is shorthand for a value binding plus an event listener, and what it binds differs per form element.
<input v-model="text"> <!-- :value="text" @input="event => text = event.target.value" -->Keeping a form field and a piece of state in step takes two halves: push the state into the element, and write the element's value back on change. v-model is shorthand for both halves at once.
What it expands to
<input v-model="text"> is the same as <input :value="text" @input="event => text = event.target.value">. Which property and which event depend on the element:
| Element | Property | Event |
|---|---|---|
<input> text types, <textarea> | value | input |
<input type="checkbox">, type="radio" | checked | change |
<select> | value | change |
The element types in practice
A lone checkbox bound to a ref gives you a boolean. Several checkboxes bound to the same array ref collect their value attributes, and the array holds whichever are checked. Radios in a group share one ref, and the selected radio's value is what lands in it. A <select> stores the chosen option's value, and multiple makes it an array. Use true-value and false-value on a checkbox when the two states are not true and false.
What trips people up: v-model treats the state as the source of truth and ignores the element's own value, checked or selected attributes for the initial render. Set the starting value on the ref, not in the markup.
Example
<!-- PreferencesForm.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('')
const subscribed = ref(false)
const topics = ref<string[]>(['vue'])
const plan = ref('free')
const country = ref('')
const countries = [
{ code: 'de', label: 'Germany' },
{ code: 'ke', label: 'Kenya' }
]
</script>
<template>
<label for="name">Name</label>
<input id="name" v-model="name" />
<!-- one checkbox, one boolean -->
<label><input type="checkbox" v-model="subscribed" /> Subscribe</label>
<!-- several checkboxes, one array: each needs a value -->
<label><input type="checkbox" value="vue" v-model="topics" /> Vue</label>
<label><input type="checkbox" value="css" v-model="topics" /> CSS</label>
<!-- radios share the ref; the chosen value lands in it -->
<label><input type="radio" value="free" v-model="plan" /> Free</label>
<label><input type="radio" value="pro" v-model="plan" /> Pro</label>
<select v-model="country">
<option value="">Choose a country</option>
<option v-for="item in countries" :key="item.code" :value="item.code">
{{ item.label }}
</option>
</select>
<p>{{ name }} / {{ subscribed }} / {{ topics.join(', ') }} / {{ plan }} / {{ country }}</p>
</template>
<style scoped>
label {
display: block;
}
</style>When to use it
- Bind a sign-up form's name, email and password fields to refs.
- Collect a set of chosen tags from a row of checkboxes.
- Store a selected country code from a dropdown built with v-for.
- Post 'yes' or 'no' from a consent checkbox without converting a boolean.
More examples
The shorthand and the longhand together
Two inputs, one ref: typing in either updates both, because they are the same pair of bindings. Write the long form when you need something extra in the write path, such as upper-casing the value or ignoring characters, and keep v-model everywhere else.
<!-- NameField.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const name = ref('')
</script>
<template>
<!-- shorthand -->
<input v-model="name" />
<!-- the same two bindings, written out -->
<input
:value="name"
@input="(event) => (name = (event.target as HTMLInputElement).value)"
/>
<p>Both inputs show: {{ name }}</p>
</template>Checkbox values that are not true and false
true-value and false-value decide what the ref holds in each state, so you do not convert a boolean on the way out. Useful when a backend expects 'yes'/'no', 1/0 or an enum. Both accept a binding, so :true-value="someValue" works as well.
<!-- ConsentToggle.vue -->
<script setup lang="ts">
import { ref } from 'vue'
// the API this form posts to expects 'yes' or 'no'
const marketing = ref<'yes' | 'no'>('no')
</script>
<template>
<label>
<input
type="checkbox"
v-model="marketing"
true-value="yes"
false-value="no"
/>
Send me product updates
</label>
<p>Will post: {{ marketing }}</p>
</template>A multiple select and a textarea
A select with multiple binds to an array, so labels holds every chosen option. The textarea line is the one people get wrong: putting {{ description }} between the tags renders nothing, because a textarea's content is not a template slot. Always bind it with v-model.
<!-- TicketFields.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const labels = ref<string[]>([])
const description = ref('')
</script>
<template>
<select v-model="labels" multiple>
<option value="bug">Bug</option>
<option value="docs">Docs</option>
<option value="perf">Performance</option>
</select>
<!-- textarea binds through v-model; interpolation inside it does nothing -->
<textarea v-model="description" rows="4"></textarea>
<p>Labels: {{ labels.join(', ') || 'none' }}</p>
<p>Characters: {{ description.length }}</p>
</template>
Discussion