Understanding v-model in Vue 3 Components - modelValue, emit, and defineModel
v-model is one of the most powerful features in Vue, enabling two-way data binding between a parent and a child component. But when you create custom components, you need to wire it up manually - and there are a few ways to do it right.
Let’s break it down with examples, including the new defineModel() helper introduced in Vue 3.3+.
The Traditional Way — modelValue + emit
When you use v-model on a custom component, Vue internally binds the modelValue prop and listens for an update:modelValue event.
Example - <BaseInput /> (Traditional Way)
<!-- BaseInput.vue -->
<template>
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</template>
<script setup lang="ts">
defineProps<{ modelValue: string }>()
defineEmits(['update:modelValue'])
</script>
Now, you can use it like this in a parent:
<BaseInput v-model="name" />
Vue handles the rest:
- Binds modelValue to name
- Listens for update:modelValue and updates name accordingly
Gotcha: You Still Need defineEmits
Even if you destructure modelValue with defineProps(), emitting the right event is on you. Misspelling ‘update:modelValue’ or not emitting it at all will break the binding.
The New Way - defineModel()
Vue 3.3+ introduces defineModel() - a powerful helper that simplifies two-way binding.
Example – <BaseInput /> (With defineModel())
<template>
<input v-model="value" />
</template>
<script setup lang="ts">
const value = defineModel<string>()
</script>
That’s it - defineModel():
- Automatically defines the modelValue prop
- Automatically emits the
update:modelValueevent when you change the bound variable - Even supports custom prop names (see below)
In parent:
<BaseInput v-model="name" />
Bonus: Custom Prop Name
Want to use a custom prop like v-model:title?
<script setup lang="ts">
const title = defineModel<string>('title')
</script>
Then use it like this:
<MyComponent v-model:title="pageTitle" />
Vue will:
- Bind to title prop
- Listen for
update:titleevent
Final Thoughts
v-model might look like magic, but under the hood, it’s just smart prop/event wiring. Use defineModel() in new code to simplify your components and reduce boilerplate.
If you’re maintaining older projects, stick with modelValue + emit, but now you know the cleaner path moving forward.
Bartłomiej Nowak
Programmer
Programmer focused on performance, simplicity, and good architecture. I enjoy working with modern JavaScript, TypeScript, and backend logic — building tools that scale and make sense.