Vue - The Road To
        Enterprise logo
The Most Advanced Vue Book Buy Now

Vue 3 Multiple v-model Bindings: defineModel and the useVModel Composable

Vue 3 form component demonstrating multiple v-model bindings with defineModel
By Thomas Findlay16 Mar 2021Updated: 2 Mar 2026

Vue 3 supports multiple v-model bindings on a single component. In Vue 3.4+, the defineModel() macro is the cleanest way to handle them. For older versions, a useVModel composable does the same job with a bit more boilerplate. This article covers both approaches. You can find the full code example in this GitHub repository.

For this example, we will use a form with name and surname fields.

Form

The parent component passes both values via v-model:name and v-model:surname:

App.vue

<template>
  <div :class="$style.container">
    <Form
      v-model:name="form.name"
      v-model:surname="form.surname"
      @submit="onSubmit"
    />
  </div>
</template>

<script setup>
import { ref } from 'vue'
import Form from './components/Form.vue'

const form = ref({
  name: '',
  surname: '',
})

const onSubmit = () => console.log(form)
</script>

<style module>
.container {
  max-width: 30rem;
  @apply mx-auto py-8;
}
</style>

defineModel() — Vue 3.4 and later

defineModel() is a compiler macro added in Vue 3.4. It declares a prop and the corresponding emit in one call, returning a writable computed ref you can bind directly with v-model.

components/Form.vue

<template>
  <form @submit.prevent="$emit('submit')">
    <div :class="$style.formBlock">
      <label :class="$style.label">Name</label>
      <input
        v-model="name"
        :class="$style.input"
        type="text"
        aria-label="Name input"
      />
    </div>
    <div :class="$style.formBlock">
      <label :class="$style.label">Surname</label>
      <input
        v-model="surname"
        :class="$style.input"
        type="text"
        aria-label="Surname input"
      />
    </div>
    <div>
      <button
        class="float-right bg-blue-100 text-blue-900 px-4 py-3 rounded font-semibold"
        type="submit"
      >
        Submit
      </button>
    </div>
  </form>
</template>

<script setup>
defineEmits(['submit'])

const name = defineModel('name')
const surname = defineModel('surname')
</script>

<style module>
.formBlock {
  @apply flex flex-col mb-4;
}
.label {
  @apply mb-2;
}
.input {
  @apply px-4 py-3 shadow rounded border border-gray-300 bg-white;
}
</style>

defineModel('name') handles everything: it declares name as a prop, registers the update:name emit, and returns a writable computed ref. When the input changes, the parent's state updates automatically. No manual emit calls, no computed getters and setters.

useVModel composable — Vue 3.0 to 3.3

If you are on a Vue version before 3.4, the useVModel composable gets the same result. To update the parent's state, the child needs to emit an update:<propName> event. The composable wraps that in a computed ref:

composables/useVModel.js

import { computed, getCurrentInstance } from 'vue'

export const useVModel = (props, propName) => {
  const vm = getCurrentInstance().proxy

  return computed({
    get() {
      return props[propName]
    },
    set(value) {
      vm.$emit(`update:${propName}`, value)
    },
  })
}

Pass props to keep reactivity intact, and the prop name you want to sync. Inside the composable, getCurrentInstance() gives access to $emit, so the setter can fire the right event. The getter returns the current prop value; the setter emits the update.

components/Form.vue (pre-3.4)

<template>
  <form @submit.prevent="$emit('submit')">
    <div :class="$style.formBlock">
      <label :class="$style.label">Name</label>
      <input
        v-model="nameState"
        :class="$style.input"
        type="text"
        aria-label="Name input"
      />
    </div>
    <div :class="$style.formBlock">
      <label :class="$style.label">Surname</label>
      <input
        v-model="surnameState"
        :class="$style.input"
        type="text"
        aria-label="Surname input"
      />
    </div>
    <div>
      <button
        class="float-right bg-blue-100 text-blue-900 px-4 py-3 rounded font-semibold"
        type="submit"
      >
        Submit
      </button>
    </div>
  </form>
</template>

<script>
import { useVModel } from '../composables/useVModel.js'
export default {
  emits: ['update:name', 'update:surname', 'submit'],
  props: {
    name: String,
    surname: String,
  },
  setup(props) {
    return {
      nameState: useVModel(props, 'name'),
      surnameState: useVModel(props, 'surname'),
    }
  },
}
</script>

@vueuse/core useVModel

If your project already uses @vueuse/core, it ships a useVModel composable that works the same way. You can skip writing your own:

<script setup>
import { useVModel } from '@vueuse/core'

const props = defineProps({
  name: String,
  surname: String,
})

defineEmits(['update:name', 'update:surname', 'submit'])

const nameState = useVModel(props, 'name')
const surnameState = useVModel(props, 'surname')
</script>

For new projects on Vue 3.4+, defineModel() is the right choice. The useVModel composable, whether custom or from VueUse, is still useful when upgrading an older codebase or when you need the extra options the VueUse version provides (like passive mode or custom event names).

Frequently asked questions


If you would like to learn more about Vue patterns and best practices, check out the Vue - The Road To Enterprise book.


Want to learn something new?

Subscribe to the newsletter!


Thomas Findlay photo

About the author

Thomas Findlay is a CTO, senior full-stack engineer, and the author of React - The Road To Enterprise and Vue - The Road To Enterprise. With 13+ years of experience, he has built and led engineering teams, architected multi-tenant SaaS platforms, and driven AI-augmented development workflows that cut feature delivery time by 50%+.

Thomas has spoken at international conferences including React Summit, React Advanced London, and Vue Amsterdam, and has written 50+ in-depth technical articles for the Telerik/Progress blog. He holds an MSc in Advanced Computer Science (Distinction) from the University of Exeter and a First-Class BSc in Web Design & Development from Northumbria University.

With a 5-star rating across 2,500+ mentoring sessions and 1,250+ reviews on Codementor, Thomas has helped developers and teams worldwide with architecture consulting, code reviews, and hands-on development support. Find him on LinkedIn, GitHub, or Twitter/X, or get in touch directly. Read the full bio →