v-model vs modelValue in Vue 3: Understanding the Directive and Prop Relationship
The v-model directive is syntactic sugar that automatically wires a two-way binding by passing data to a modelValue prop and listening for an update:modelValue event, while modelValue itself is merely a standard component prop that receives that data.
In Vue 3, the distinction between the template-level directive and the component-level prop is fundamental to how two-way binding operates. While the v-model directive handles the coordination logic in the runtime, the modelValue prop serves as the actual data container defined on the child component.
What is the v-model Directive?
The v-model directive is a template-level abstraction that lives in Vue’s runtime compilation system. When the compiler encounters v-model="foo" on a component, it rewrites the expression to bind the value as a prop and attach an event listener that updates the parent’s state.
According to the Vue 3 source code, the directive’s implementation resides in packages/runtime-core/src/directives/vModel.ts. This file exports the directive definition containing beforeMount, mounted, updated, and unmounted hooks that manage the synchronization between the DOM element (or child component) and the reactive data. The directive handles the automatic propagation of value changes without requiring manual event wiring in the template.
What is the modelValue Prop?
modelValue is a standard Vue component prop—no different from title or disabled—that serves as the default target for v-model bindings. In packages/runtime-core/src/component.ts, the core logic processes component props, including modelValue, as part of the normal options API or setup function handling.
When you accept v-model in a child component, you explicitly declare modelValue (or a custom name) in your props definition and emit the corresponding update:modelValue event to signal changes. The prop itself holds no special internal behavior; it is simply the conventional name that the v-model directive expects when no custom argument is provided.
How v-model and modelValue Work Together
During template compilation, Vue transforms v-model syntax into explicit prop binding and event listening. This transformation bridges the directive layer and the component API layer.
For a default binding:
<MyInput v-model="text" />
Vue compiles this to the equivalent of:
<MyInput :modelValue="text" @update:modelValue="value => text = value" />
When using a custom prop name via the argument syntax:
<MyInput v-model:checked="isChecked" />
This compiles to:
<MyInput :checked="isChecked" @update:checked="value => isChecked = value" />
Thus, the directive (v-model) belongs to the template compilation/runtime layer, automatically generating the boilerplate to keep parent and child state synchronized, while the prop (modelValue or checked) belongs to the component API layer, acting as the passive receiver of that data.
Implementation Examples
The following patterns demonstrate the separation of concerns in practice.
Parent Component (Template):
<!-- default modelValue binding -->
<CustomInput v-model="username" />
<!-- custom prop name binding -->
<CustomCheckbox v-model:checked="agreed" />
Child Component (Options API) in CustomInput.vue:
export default {
props: {
// Default prop for v-model
modelValue: String,
// Custom prop when using v-model:checked
checked: Boolean
},
emits: ['update:modelValue', 'update:checked'],
methods: {
onInput(e) {
// Emit for default v-model
this.$emit('update:modelValue', e.target.value)
},
onToggle(newState) {
// Emit for custom v-model
this.$emit('update:checked', newState)
}
}
}
Child Component (Composition API) in CustomInput.vue:
import { defineComponent, toRefs } from 'vue'
export default defineComponent({
props: {
modelValue: String,
checked: Boolean
},
emits: ['update:modelValue', 'update:checked'],
setup(props, { emit }) {
const { modelValue, checked } = toRefs(props)
const updateValue = (val) => emit('update:modelValue', val)
const updateChecked = (val) => emit('update:checked', val)
return { modelValue, checked, updateValue, updateChecked }
}
})
These examples confirm that v-model never accesses the prop directly; it only wires the prop and the event together to achieve two-way reactivity.
Summary
v-modelis a directive inpackages/runtime-core/src/directives/vModel.tsthat provides syntactic sugar for two-way bindings.modelValueis a conventional prop name defined inpackages/runtime-core/src/component.tsthat receives the bound value.- The directive automatically compiles to
:modelValueand@update:modelValue(or custom equivalents when usingv-model:propName). - Custom prop names allow multiple
v-modelbindings on a single component using thev-model:customPropsyntax. - The child component must declare both the prop and the corresponding
update:event in itsemitsoption.
Frequently Asked Questions
Is modelValue a reserved or magical property in Vue 3?
No. modelValue is merely a naming convention adopted by the v-model directive when no custom argument is provided. It functions identically to any other component prop and receives no special treatment from the reactivity system beyond what standard prop validation provides.
Can I use multiple v-model bindings on a single component?
Yes. Vue 3 supports multiple v-model bindings using the argument syntax. You can declare v-model:title="postTitle" and v-model:content="postContent" simultaneously, provided the child component defines both title and content props and emits both update:title and update:content events.
Do I need to declare update:modelValue in the emits option?
Yes, explicit declaration is recommended. While Vue 3 will still function if you emit undeclared events, declaring update:modelValue (or your custom update event) in the emits option enables better IDE support, type checking, and avoids warnings during development regarding unexpected events.
Where does the v-model directive handle DOM elements versus components?
The implementation in packages/runtime-core/src/directives/vModel.ts includes separate handling paths for native form elements (input, select, textarea) and custom components. For native elements, it directly attaches event listeners to the DOM node; for components, it relies on the emitted update:modelValue event mechanism described above.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →