Files
stack/packages/mosaic/framework/skills/vue-best-practices/reference/v-model-vue3-breaking-changes.md
fargo 1a822493ba format: apply repo prettier (3.8.1) to the folded skills tree
963 markdown files reformatted with the repository's pinned prettier so
pnpm format:check covers the folded tree like every other repo file.

The formatter's embedded-language pass also normalized code fences
(TS semicolons, closed HTML tags in examples, lowercased CSS hex colors,
one renumbered list that skipped an index). Alphanumeric token deltas vs
the fold commit were audited file-by-file; all are formatter-equivalent
markup normalizations plus the four sanitized skills.
2026-08-19 14:37:17 -05:00

4.3 KiB

title, impact, impactDescription, type, tags
title impact impactDescription type tags
v-model Breaking Changes from Vue 2 to Vue 3 HIGH Vue 3 changed v-model prop/event names and removed .sync modifier - migration required migration
vue3
vue2
v-model
migration
breaking-changes
sync

v-model Breaking Changes from Vue 2 to Vue 3

Impact: HIGH - Vue 3 fundamentally changed how v-model works on custom components. Code using the Vue 2 pattern will silently fail - the component won't receive the prop value and changes won't propagate to the parent.

Task Checklist

  • Change prop name from value to modelValue
  • Change event name from input to update:modelValue
  • Replace .sync modifier with v-model:propName
  • Remove model component option (use defineModel or named v-model)
  • Update any v-bind.sync to v-model with named argument

Key Breaking Changes

Feature Vue 2 Vue 3
Default prop value modelValue
Default event input update:modelValue
Custom name model: { prop, event } v-model:customName
Sync modifier v-bind:prop.sync v-model:prop
Multiple models Not supported Fully supported

Vue 2 Pattern (No longer works in Vue 3):

<!-- Vue 2 Child Component -->
<script>
export default {
  props: ['value'], // WRONG in Vue 3
  methods: {
    update(val) {
      this.$emit('input', val); // WRONG in Vue 3
    },
  },
};
</script>

<template>
  <input :value="value" @input="update($event.target.value)" />
</template>

Vue 3 Pattern (Options API):

<!-- Vue 3 Child Component -->
<script>
export default {
  props: ['modelValue'], // Changed from 'value'
  emits: ['update:modelValue'], // Declare emits
  methods: {
    update(val) {
      this.$emit('update:modelValue', val); // Changed from 'input'
    },
  },
};
</script>

<template>
  <input :value="modelValue" @input="update($event.target.value)" />
</template>

Vue 3 Pattern (Composition API with defineModel):

<!-- Vue 3 Child Component - Recommended -->
<script setup>
const model = defineModel(); // Handles prop and emit automatically
</script>

<template>
  <input v-model="model" />
</template>

Migrating the .sync Modifier

Vue 2's .sync modifier is removed. Use named v-model instead.

Vue 2:

<!-- Parent -->
<MyComponent :title.sync="pageTitle" />

<!-- Child -->
<script>
export default {
  props: ['title'],
  methods: {
    updateTitle(val) {
      this.$emit('update:title', val); // .sync pattern
    },
  },
};
</script>

Vue 3:

<!-- Parent -->
<MyComponent v-model:title="pageTitle" />

<!-- Child with defineModel -->
<script setup>
const title = defineModel('title');
</script>

<template>
  <input v-model="title" />
</template>

<!-- Child with manual props/emits -->
<script setup>
const props = defineProps(['title']);
const emit = defineEmits(['update:title']);
</script>

<template>
  <input :value="props.title" @input="emit('update:title', $event.target.value)" />
</template>

Migrating Custom model Option

Vue 2's model component option is removed.

Vue 2:

<script>
export default {
  model: {
    prop: 'checked',
    event: 'change',
  },
  props: ['checked'],
};
</script>

Vue 3:

<!-- Use named v-model argument instead -->
<!-- Parent -->
<MyCheckbox v-model:checked="isChecked" />

<!-- Child -->
<script setup>
const checked = defineModel('checked');
</script>

Multiple v-model Bindings (New in Vue 3)

Vue 3 allows multiple v-model directives on a single component:

<!-- Parent -->
<UserForm v-model:firstName="first" v-model:lastName="last" v-model:email="email" />

<!-- Child -->
<script setup>
const firstName = defineModel('firstName');
const lastName = defineModel('lastName');
const email = defineModel('email');
</script>

<template>
  <input v-model="firstName" placeholder="First Name" />
  <input v-model="lastName" placeholder="Last Name" />
  <input v-model="email" type="email" placeholder="Email" />
</template>

Reference