Suggest an editImprove this articleRefine the answer for “V-model in Vue.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**v-model** is Vue's directive for two-way data binding between a form input and component state. ```vue <input v-model="text" /> <!-- Compiles to --> <input :value="text" @input="text = $event.target.value" /> <!-- Custom component --> <CustomInput v-model="text" /> // Child: defineProps(['modelValue']), defineEmits(['update:modelValue']) ``` **Key:** syntactic sugar over `:value` + `@input`. For custom components, use `modelValue` prop with `update:modelValue` emit. Modifiers: `.lazy`, `.number`, `.trim`.Shown above the full answer for quick recall.Answer (EN)Image**v-model** is Vue's directive for two-way data binding on form inputs, syncing component state with user input through a value prop and input event. ## Theory ### TL;DR - `v-model` compiles to `:value="x" @input="x = $event.target.value"` under the hood - Works on native inputs (`<input>`, `<textarea>`, `<select>`) and custom components - Custom components: expects `modelValue` prop and `update:modelValue` emit - Vue 3 allows multiple named v-models on one component: `v-model:firstName`, `v-model:lastName` - Modifiers `.lazy`, `.number`, `.trim` handle the most common input transformations ### Quick example ```vue <template> <input v-model="message" placeholder="Type here" /> <p>Live: {{ message }}</p> </template> <script setup> import { ref } from 'vue' const message = ref('Hello Vue') // Initial value shows in the input </script> ``` Typing in the input updates `message` instantly. The `<p>` tag re-renders automatically because `message` is a reactive ref. No event handler needed. ### How it compiles Vue's template compiler transforms `v-model="text"` at build time: ```vue <!-- What you write --> <input v-model="text" /> <!-- What the compiler produces --> <input :value="text" @input="text = $event.target.value" /> ``` For custom components the output differs: ```vue <!-- What you write --> <CustomInput v-model="text" /> <!-- What the compiler produces --> <CustomInput :modelValue="text" @update:modelValue="text = $event" /> ``` The reactivity system (Proxy-based in Vue 3) tracks the assignment and triggers re-renders for anything that reads `text`. ### When to use - `<input>`, `<textarea>`, `<select>` - use v-model directly - Custom input components - v-model via `modelValue` + `update:modelValue` - Multiple fields on one component - named v-models like `v-model:firstName` - Non-form elements like buttons - skip v-model, use `@click` or manual binding - Complex forms with state shared across many components - consider Pinia instead of chaining v-models ### Modifiers Three built-in modifiers cover the most common input processing needs: ```vue <input v-model.lazy="text" /> <!-- Updates on blur, not every keystroke --> <input v-model.number="age" /> <!-- Casts string to number automatically --> <input v-model.trim="username" /> <!-- Strips leading/trailing whitespace --> <!-- Combine them --> <input v-model.lazy.trim="email" /> ``` `.lazy` switches the listener from `@input` to `@change`. Useful for search fields where you want to avoid firing a query on every single keystroke. ### v-model on custom components The parent passes a value, the child emits updates back. That is the whole contract. ```vue <!-- Parent --> <CustomInput v-model="searchText" /> <!-- Child: CustomInput.vue --> <template> <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" /> </template> <script setup> defineProps(['modelValue']) defineEmits(['update:modelValue']) </script> ``` A cleaner approach wraps the logic in a computed with getter and setter. The computed getter/setter pattern is what I reach for most often in real components - it keeps the template clean and makes the data flow readable at a glance. ```vue <!-- CustomInput.vue --> <script setup> import { computed } from 'vue' const props = defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) const value = computed({ get: () => props.modelValue, set: (val) => emit('update:modelValue', val) }) </script> <template> <input v-model="value" /> </template> ``` ### Multiple v-model bindings (Vue 3) Vue 3 removed the single-binding restriction from Vue 2. Each named `v-model:propName` maps to a `propName` prop and an `update:propName` emit: ```vue <!-- Parent --> <UserName v-model:firstName="firstName" v-model:lastName="lastName" /> <!-- Child: UserName.vue --> <template> <input :value="firstName" @input="$emit('update:firstName', $event.target.value)" placeholder="First name" /> <input :value="lastName" @input="$emit('update:lastName', $event.target.value)" placeholder="Last name" /> </template> <script setup> defineProps(['firstName', 'lastName']) defineEmits(['update:firstName', 'update:lastName']) </script> ``` ### Common mistakes **Mutating a prop directly.** This is the most common v-model bug in Vue components. ```vue <!-- Wrong: direct prop mutation --> <template> <input v-model="modelValue" /> </template> <script setup> defineProps(['modelValue']) // Vue warns: "Avoid mutating a prop directly" </script> ``` Props flow one direction by design. The parent owns the data, and changes must travel back through emit. Fix it with computed: ```vue <!-- Correct: computed as a proxy --> <script setup> import { computed } from 'vue' const props = defineProps(['modelValue']) const emit = defineEmits(['update:modelValue']) const value = computed({ get: () => props.modelValue, set: (val) => emit('update:modelValue', val) }) </script> ``` **Using v-model on contenteditable.** It does nothing. ```vue <!-- No effect --> <div contenteditable v-model="text"></div> ``` `v-model` expects a `value` property to bind, which `contenteditable` elements do not have. Wire it manually: ```vue <div contenteditable="true" :innerHTML="text" @input="text = $event.target.innerText" ></div> ``` **Wrong ref type for multiple checkboxes.** When binding several checkboxes to one ref, the ref must be an array. ```vue <script setup> // Wrong: string ref breaks toggle logic const hobbies = ref('') // Correct: array tracks all checked values const hobbies = ref([]) </script> <template> <input type="checkbox" v-model="hobbies" value="coding" /> Coding <input type="checkbox" v-model="hobbies" value="skiing" /> Skiing <p>{{ hobbies }}</p> <!-- ['coding', 'skiing'] when both checked --> </template> ``` With an array, Vue pushes and splices values as boxes are toggled. With a string, behavior is undefined. ### Real-world usage - Vuetify: `<v-text-field v-model="form.email">` in admin dashboards - Quasar: `<q-input v-model="state.search">` for search in mobile apps - Element Plus: `<el-input v-model="query">` in data tables with server-side filtering - VeeValidate: wraps `v-model` to validate before emitting the update - Nuxt.js: `v-model` on user profile forms combined with `useAsyncData` for SSR-safe inputs ### Follow-up questions **Q:** What is the compiled output of `<input v-model="msg" />`? **A:** `:value="msg" @input="msg = $event.target.value"`. For a custom component it becomes `:modelValue="msg" @update:modelValue="msg = $event"`. **Q:** How does v-model differ between Vue 2 and Vue 3? **A:** Vue 2 uses the `value` prop and `input` event, hardcoded for all components. Vue 3 uses `modelValue` and `update:modelValue`, which enables multiple named v-models like `v-model:foo` on a single component. **Q:** What event does v-model listen to on `<select multiple>`? **A:** `change`, not `input`. Vue handles this automatically and collects the selected options into an array. **Q:** How do you implement a lazy v-model on a custom component, updating only on blur? **A:** Store the intermediate value in a local `ref` on `@input`, then emit `update:modelValue` only in `@blur`. This mirrors `v-model.lazy` behavior but inside a custom component. **Q:** Two inputs share `v-model="text"`. What happens? **A:** Race condition on `@input`. Whichever fires last wins, so the second input always overwrites the first. Use separate refs or a computed with getter/setter per field. ## Examples ### Basic: live search input ```vue <template> <input v-model="query" placeholder="Search products..." /> <p>Searching for: {{ query }}</p> </template> <script setup> import { ref } from 'vue' const query = ref('') </script> ``` Every keystroke updates `query` and the paragraph re-renders. No event handler boilerplate needed. ### Intermediate: todo form (TodoMVC pattern) ```vue <template> <input v-model="newTodo" @keyup.enter="addTodo" placeholder="Add a todo..." /> <ul> <li v-for="todo in todos" :key="todo.id">{{ todo.text }}</li> </ul> </template> <script setup> import { ref } from 'vue' const newTodo = ref('') const todos = ref([]) function addTodo() { if (!newTodo.value.trim()) return todos.value.push({ id: Date.now(), text: newTodo.value.trim() }) newTodo.value = '' // v-model clears the input when you reset the ref } </script> ``` Press Enter: the todo appears in the list, the input clears. Setting `newTodo.value = ''` is enough because v-model keeps the DOM in sync with the ref. ### Advanced: custom email input with validation and lazy update ```vue <!-- EmailInput.vue --> <template> <input :value="modelValue" @blur="handleBlur" @input="localValue = $event.target.value" :class="{ error: hasError }" /> <span v-if="hasError">Invalid email</span> </template> <script setup> import { ref } from 'vue' const props = defineProps({ modelValue: String }) const emit = defineEmits(['update:modelValue']) const localValue = ref(props.modelValue) const hasError = ref(false) function handleBlur() { hasError.value = !localValue.value.includes('@') if (!hasError.value) { emit('update:modelValue', localValue.value) // Only emits on valid blur } } </script> ``` The parent's data updates only when the user leaves the field and the value passes validation. Typing stays local until blur. This pattern appears often in form libraries like VeeValidate.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.