Cansu Arı Logo
Blog
What is it?

The Hidden Anatomy of v-model — modelValue and update:modelValue

Vue’s v-model may look like a magical directive, but it’s actually a smart contract that manages two-way data flow.

  • #Vue.js

Skip to content

modelValue and update:modelValue

Everyone new to Vue loves v-model. Binding a value to an input couldn’t be easier. But if you don’t understand what happens behind the scenes, v-model won’t work on your own components—and the magic breaks. 🙃

In this post we’ll explain how v-model works step by step, and what modelValue and update:modelValue actually represent.

v-model in a Nutshell

At its simplest:
<input v-model="name" />
Vue compiles this to:
<input :value="name" @input="name = $event.target.value" />
So v-model is shorthand for a :value binding plus an @input listener. But that’s only for native HTML inputs. Things change with custom components. 👇

How Does v-model Work on Components?

When you use v-model on your own component, Vue automatically looks for two things:
  1. A prop named modelValue,
  2. An emit named update:modelValue.
<!-- Parent -->
<MyInput v-model="username" />
<!-- MyInput.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<input :value="modelValue" @input="emit('update:modelValue', $event.target.value)" />
</template>

Now v-model behaves just like on a native input: the parent sends the value (via props), the child reports changes (via emit).

What’s Happening Internally?

v-model is a data-flow protocol with two directions:
  • Downstream (parent → child): value flows through the modelValue prop.
  • Upstream (child → parent): changes are reported via the update:modelValue event.
Vue wires this up transparently. The parent keeps username reactive, and that state is automatically bound to the child input’s value.

In short: v-model turns “props in, emits out” into a two-way contract.

Renaming Is Possible

Sometimes you want multiple v-models (e.g., one for value, one for checked). Vue supports that too:
<MyToggle v-model:checked="isOn" v-model:label="title" />
<script setup>
const props = defineProps(['checked', 'label'])
const emit = defineEmits(['update:checked', 'update:label'])
</script>
Each v-model pairs with its own prop/event duo.

💡 Tip: Multiple v-models are great for complex form components—e.g., controlling both “value” and “valid” state.

What Does the Compiler Actually Generate?

When the template compiler sees v-model, it translates it to this pattern:
<MyInput :modelValue="username" @update:modelValue="username = $event" />
No extra code needed. But if your component doesn’t declare defineEmits(['update:modelValue']), Vue will warn: > Missing required emit event: update:modelValue

So this convention is what makes v-model work.

Advanced: Computed v-model

Sometimes you want to transform the parent’s value inside the component. Use a computed getter/setter:
const message = ref('Hello')
const upper = computed({
get: () => message.value.toUpperCase(),
set: val => message.value = val.toLowerCase()
})
With v-model="upper", the input always displays uppercase but stores lowercase. 😎

v-model Modifiers

Vue supports modifiers like .trim, .number, .lazy:
<input v-model.trim.number="age" />
These are tiny transforms around the input event—v-model.number is like applying parseFloat($event.target.value).

.lazy hooks into change instead of input, useful for performance or validation.

When to Use Multiple v-models?

To manage multiple states (e.g., value, error, valid) in form components:
<MyField v-model:value="text" v-model:valid="isValid" />
This pattern is common in validation libraries—each pair represents an independent data stream.

Real-World Example

A simple modal component:
<Modal v-model:open="isOpen" />
Child:
<script setup>
const props = defineProps(['open'])
const emit = defineEmits(['update:open'])
function close(){ emit('update:open', false) }
</script>
When isOpen = false in the parent, the modal closes; when the modal closes itself, the parent updates too. Two-way control complete.

Common Pitfalls

  1. Forgetting defineEmits(['update:modelValue']).
  2. Manually writing both :modelValue and @update:modelValue where v-model would suffice.
  3. Trying to mutate a prop (props.modelValue = 'x') → readonly error.
  4. Destructuring props (const { modelValue } = props) can break reactivity when used carelessly.

Summary Table

TermMeaning
v-modelTwo-way binding shorthand
modelValueData flowing in from the parent
update:modelValueEvent flowing out from the child
v-model:customMultiple model support
v-model.trimTrims input text
v-model.lazyListens on change instead of input

Conclusion

v-model is an elegant data-exchange protocol in Vue’s reactivity architecture. It might look like just a prop/emit pair, but it standardizes component communication in a powerful way.

In short:

  • modelValue → data in
  • update:modelValue → data out
  • v-model → binds the two automatically

Once you internalize this, any component can behave like a “form element,” and complex data flows become effortless.

All tags
  • Vue.js