In Vue.js, the v-model directive allows you to create a bidirectional binding between an HTML form element and a JavaScript variable in the Vue instance.
The bidirectional binding works both ways, from the data in JavaScript to HTML and vice versa.
That is,
- Any change in the form element ➡️ will be reflected in the data
- Any change in the data ➡️ will be reflected in the form element.
In the previous tutorial, we saw unidirectional binding, from attributes, which works only from JS to HTML, and is done with the v-bind directive.
v-model Directive
As we mentioned, the v-model directive allows for bidirectional binding that works between JavaScript data and HTML.
The binding occurs between a form element (such as <input>, <textarea>, or <select>), which are the elements that can receive user input.
The binding with v-model allows changes in the form element to automatically reflect in the data, and vice versa.
<template>
  <input v-model="message" placeholder="Type something" />
  <p>Message: {{ message }}</p>
</template>
<script setup>
import { ref } from 'vue';
const message = ref('');
</script>In this example:
- The value of the text field is bound to the variable message.
- If the user types in the field, messageis automatically updated.
- If messagechanges in the script, the text field is automatically updated.
Using v-model with Different Form Elements
The v-model behaves (slightly) differently depending on the type of form element to which we apply it.
Depending on the element we use it on, it will be,
| Element | Bound Attribute | Data Type | Behavior | 
|---|---|---|---|
| <input>(text) | value | String | The input value is bound to the property. Updates bidirectionally. | 
| <textarea> | value | String | The content of the textarea is bound to the property. Updates bidirectionally. | 
| <select> | valueof the selected option | String (or array) | The selected value is bound to the property. In multiple, it is an array. | 
| <input>(checkbox) | checked | Boolean (or array if multiple) | The checked state is bound to the property. In multiple, it is an array. | 
| <input>(radio) | checked | String | The value of the selected option is bound to the property. Only one option can be selected. | 
Vue handles the binding correctly to ensure that the data is updated properly in both directions.
<template>
  <input v-model="text" type="text" placeholder="Type something" />
  <p>Entered text: {{ text }}</p>
</template>
<script setup>
import { ref } from 'vue';
const text = ref('');
</script><template>
  <textarea v-model="description" placeholder="Write a description"></textarea>
  <p>Description: {{ description }}</p>
</template>
<script setup>
import { ref } from 'vue';
const description = ref('');
</script><template>
  <select v-model="selectedOption">
    <option value="option1">Option 1</option>
    <option value="option2">Option 2</option>
    <option value="option3">Option 3</option>
  </select>
  <p>Selected option: {{ selectedOption }}</p>
</template>
<script setup>
import { ref } from 'vue';
const selectedOption = ref('option1');
</script><template>
  <label>
    <input type="checkbox" v-model="isChecked" />
    Do you agree?
  </label>
  <p>Checkbox status: {{ isChecked ? 'Yes' : 'No' }}</p>
</template>
<script setup>
import { ref } from 'vue';
const isChecked = ref(false);
</script>v-model Modifiers
The v-model modifiers allow you to adjust the behavior of data synchronization between the state and the DOM in Vue.js.
| Modifier | Description | 
|---|---|
| .lazy | Updates the value only after the changeevent occurs | 
| .number | Automatically converts the input value to a number | 
| .trim | Removes whitespace from the beginning and end of the value. | 
The .lazy modifier makes v-model update the value only after the change event (instead of input).
<template>
  <input v-model.lazy="message" placeholder="Type something" />
  <p>Message: {{ message }}</p>
</template>
<script setup>
import { ref } from 'vue';
const message = ref('');
</script>In this example:
- The value of messageis updated only when the text field loses focus.
The .number modifier automatically converts the input to a number.
<template>
  <input v-model.number="age" type="number" placeholder="Age" />
  <p>Age: {{ age }} ({{ typeof age }})</p>
</template>
<script setup>
import { ref } from 'vue';
const age = ref(0);
</script>In this example:
- The input is automatically converted to a number.
The .trim modifier removes whitespace from the beginning and end of the input.
<template>
  <input v-model.trim="username" placeholder="Username" />
  <p>Username: "{{ username }}"</p>
</template>
<script setup>
import { ref } from 'vue';
const username = ref('');
</script>In this example:
- Whitespace from the beginning and end is automatically removed.
It is possible to combine multiple modifiers for more precise behavior.
<input v-model.lazy.trim.number="amount" />Using v-bind in Our Components Advanced
v-model can also be used in our own components to create a bidirectional binding between the parent component and the child component.
CustomInput.vue
<template>
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script setup>
defineProps({
  modelValue: String,
});
defineEmits(['update:modelValue']);
</script>ParentComponent.vue
<template>
  <CustomInput v-model="message" />
  <p>Message: {{ message }}</p>
</template>
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
const message = ref('');
</script>In this example:
- The child component (CustomInput) receives the propmodelValueand emits anupdate:modelValueevent when the value changes.
- The parent component uses v-modelto bindmessagewith the child component.
