Hello and welcome back!
In our last lesson, we completed our understanding of the fundamental "props down, events up" communication pattern by implementing the delete functionality in our to-do app. You learned how a child component can emit an event (with a data payload) to notify its parent to perform an action.
Today, we're going to build directly on that knowledge to learn about one of Vue's most powerful and convenient features: using the v-model directive on your own custom components. You've already used v-model on native HTML inputs, and now you'll learn how to make your own components work with it to achieve seamless two-way data binding. This is a key technique for creating clean, reusable form elements.
This lesson directly addresses your goal of understanding best practices by showing you the modern, elegant way to handle two-way data binding, while also explaining the underlying mechanics so you know why it works.
By the end of this lesson, you will be able to use v-model on a custom component to create a reusable, two-way-bound input, which will make your parent components cleaner and your custom components more intuitive to use.
Part 1: Deconstructing v-model
Before we can use v-model on our own components, we need to be crystal clear on what it's actually doing. At its core, v-model is just "syntactic sugar"—a shorthand for a more verbose pattern.
On a native HTML <input> element, v-model is a shortcut for binding the value property and listening for the input event to update your state.
So, this:
<input v-model="searchText">
Is just a shorter way of writing this:
<input :value="searchText" @input="searchText = $event.target.value">
The video below gives a fantastic breakdown of this concept. Understanding this is the key to unlocking v-model on custom components.
Learn Vue 3 - Ep 12, A Deeper Look at v-model
Watch the first part of this video from Laracasts, titled 'Learn Vue 3 - Ep 12, A Deeper Look at v-model'. It does an excellent job of explaining what v-model is a shorthand for on native form inputs.
Watch from the beginning up to 02:42. Focus on how v-model is deconstructed into a :value binding and an @input listener.
Now, how does this apply to custom components? The principle is the same, but the names are different. When you use v-model on a component:
<CustomInput v-model="searchText" />
It expands to this:
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
As you can see, it's just a prop and an event!
- The Prop: It passes a prop named
modelValue. - The Event: It listens for a custom event named
update:modelValue.
This directly connects to our previous lesson on props and events. To make a component v-model-compatible, it simply needs to accept a modelValue prop and emit an update:modelValue event when it wants to change the value.
Part 2: The "Classic" Implementation (Props & Emits)
To solidify this concept, let's look at how you would manually implement this modelValue/update:modelValue contract. This was the standard way to create v-model components for a long time, and understanding it is crucial for grasping what's happening under the hood.
To see this in action, let's watch another short video.
Vue v-model in child components and defineModel
This video, 'Vue v-model in child components and defineModel' by Andre Madarang, starts by demonstrating exactly how to implement the classic props and emits pattern to support v-model.
Watch from 02:25 to 06:58. The video clearly shows how to accept the modelValue prop and emit the update:modelValue event for both a text input and a checkbox. This is the 'manual way' of doing things.
Let's summarize the code for a simple text input component.
Child Component (CustomInput.vue)
<script setup>
// 1. Accept the 'modelValue' prop from the parent
const props = defineProps(['modelValue'])
// 2. Declare that we will emit the 'update:modelValue' event
const emit = defineEmits(['update:modelValue'])
</script>
<template>
<!-- 3. Bind the input's value to the prop -->
<input
:value="props.modelValue"
<!-- 4. On user input, emit the update event with the new value -->
@input="emit('update:modelValue', $event.target.value)"
/>
</template>
This works perfectly, but it involves a bit of boilerplate code: defining the prop, defining the emit, and wiring them up in the template. As your components get more complex, this can become verbose.
Part 3: The Modern Way: defineModel()
Because this pattern is so common, Vue 3.4 introduced a much simpler and more direct way to accomplish the same thing: the defineModel() macro.
This macro does all the work of setting up the prop and the event listener for you, turning multiple lines of code into just one.
Child Component (CustomInput.vue) with defineModel()
<script setup>
// This single line replaces defineProps and defineEmits!
const model = defineModel()
</script>
<template>
<!-- The `model` ref can be used directly with v-model -->
<input v-model="model" />
</template>
That's it! defineModel() returns a ref that is automatically synchronized with the parent's v-model. When the parent's value changes, the model ref updates. When you change the model ref in the child (for example, by tying it to the input), it automatically emits the update:modelValue event.
This visual comparison makes the benefit strikingly clear:

To see defineModel in action and how it simplifies the code from the previous section, let's return to the same video.
Vue v-model in child components and defineModel
Let's watch the final part of Andre Madarang's video, where he refactors the component to use defineModel().
Watch from 14:21 to 16:29. This section demonstrates how to enable and use the defineModel macro, drastically simplifying the code.
For an official reference, the Vue documentation is the best place to look.
The official Vue documentation provides a concise explanation of defineModel() and also shows the 'Under the Hood' implementation that we've just discussed.
Read the sections 'Basic Usage' and 'Under the Hood'. This will reinforce what defineModel() does and how it relates to the manual props/emits implementation.
Part 4: Mini-Project: Creating a Reusable BaseInput
Now it's time for some hands-on practice. Let's create a reusable input component for our to-do app using defineModel(). We'll use it to replace the input for adding new tasks.
Step 1: Create BaseInput.vue
- In your
src/componentsfolder, create a new file namedBaseInput.vue. - Inside this file, implement a simple wrapped input using
defineModel(). You can also add some basic styling to make it look distinct.
Your BaseInput.vue should look something like this:
<!-- src/components/BaseInput.vue -->
<script setup>
const model = defineModel()
</script>
<template>
<input class="base-input" type="text" v-model="model" />
</template>
<style scoped>
.base-input {
width: 100%;
padding: 8px 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
</style>
Step 2: Use BaseInput in App.vue
- Open
src/App.vue. - Import your new
BaseInputcomponent. - In the template, replace the existing
<input>element for adding new to-dos with your<BaseInput>component. - Use
v-modelto bind it to the samerefyou were using before (e.g.,newTodoText).
Your App.vue script should be updated to import the new component:
// src/App.vue
import { ref } from 'vue';
import TodoItem from './components/TodoItem.vue';
import BaseInput from './components/BaseInput.vue'; // <-- Import BaseInput
// ... existing refs for todos, newTodoText, etc.
And your App.vue template's form area should be changed from this:
<!-- Old version -->
<input v-model="newTodoText" @keyup.enter="addTodo" placeholder="Add a new task">
To this:
<!-- New version -->
<BaseInput v-model="newTodoText" @keyup.enter="addTodo" placeholder="Add a new task" />
Notice that we can still listen for native keyboard events like @keyup.enter on our custom component, and Vue is smart enough to apply that listener to the root element inside the component. The placeholder is also passed down as an attribute.
Now, run your app. The input for adding new tasks should look and function the same, but you're now using your own reusable, two-way-bound component!
Conclusion
Great work today! You've taken a deep dive into v-model and learned how to make your own components benefit from its clean and intuitive syntax. This is a huge step towards building professional, reusable component libraries.
Key Takeaways:
v-modelis Syntactic Sugar: On custom components, it's a shortcut for passing a:modelValueprop and listening for an@update:modelValueevent.- The Classic Pattern: You can support
v-modelmanually by usingdefinePropsanddefineEmitsto implement themodelValue/update:modelValuecontract. - The Modern Standard: The
defineModel()macro is the recommended, modern way to implementv-modelin a child component. It's concise, clear, and powerful. - Reusability: Creating custom form components with
v-modelis a cornerstone of building scalable and maintainable Vue applications.
In our next lesson, we will explore another powerful feature for component composition: slots. You'll learn how to create flexible layout components that can accept and render content provided by the parent, giving you even more control over how you build your user interfaces.