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:modelValue event 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:title event

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

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.

Recent Posts