Using v-model on custom components - the old way and the new way

Using v-model on custom components - the old way and the new way

21 May 2025 javascript model vue

If you're reading this you'll be aware that Vue supports two-way binding on fields and custom components via its v-model directive.

However, it's always been a bit clunky to use on custom components because it was designed for use on native form elements. The defineModel() macro, however, which landed in Vue 3.4 and is available in the Composition API (as opposed to the Options API), greatly improves matters.

The old way 🔗

Before Vue 3.4, here's how we used models on a custom component.

App.vue:

<template> <p>Model value is: {{fieldVal}}</p> <Field v-model='fieldVal' /> </template> <script setup> import { ref } from 'vue' import Field from './Field.vue' const fieldVal = ref(); </script>

Field.vue:

<template> <input :value='modelValue' @input='evt => $emit("update:modelValue", evt.target.value)' > </template> <script setup> const props = defineProps(['modelValue']); const emits = defineEmits(['update:modelValue']); </script>

Here's a demo of that working. This works because, under the hood, when v-model is used on a custom component, Vue actually unpacks it to this:

<Field :model-value='fieldVal' @update:model-value='val => fieldVal = val' />

Looked at this way, it's clear what our custom component needs to do. Firstly, it needs to receive the model-value prop (which arrives in JS as the camel-cased modelValue), which it then binds to the native input's value attribute to set the initial value. Secondly, it needs to emit back up to the parent when the value is changed, so the parent can update its own fieldVal ref.

If we want to use another prop name, we can stipulate this by passing an argument to v-model, by specifying the prop name we want to use after a colon. (We'd do this, say, if we wanted to have multiple models on the same custom component.)

<Field v-model:foo='fieldVal' />

Now, our custom component would need to use foo, not modelValue, in its prop and emit names.

The new way 🔗

This always seemed more work than it need be. As of Vue 3.4, though, we now have the defineModel() macro. This replaces the explicit declaration of the model prop and emit in the custom component, which now becomes simply:

<template> <input v-model='model'> </template> <script setup> const model = defineModel(); </script>

Here's a demo of that working. How much simpler is that?

Note also how, unlike before, we can use v-model our native input to bind it to model. We couldn't do this before, because what we had instead was a prop, and if you try to v-model to a prop, you'll get an error, since props are read-only.

<template> <input v-model='modelValue'> <!-- <== ERROR! --> </template> <script setup> const props = defineProps(['modelValue']); const emits = defineEmits(['update:modelValue']); </script>

Like before, we can once again chage the name of the model, this time by passing a string as the first argument to the macro. So if our parent component did:

<Field v-model:trumpet='fieldVal' />

...our custom component would respond via:

const model = defineModel('trumpet'); //<-- note, argument

So there you have it: defineModel() is now the preferred means of using models with custom components, assuming you're using the Composition API. If you're still using the Options API, the old method will still work and isn't going away any time soon.

Did I help you? Feel free to be amazing and buy me a coffee on Ko-fi!