Fix: Vue v-model Not Working on Custom Components — Prop Not Syncing
Quick Answer
How to fix Vue v-model on custom components — defineModel, modelValue/update:modelValue pattern, multiple v-model bindings, v-model modifiers, and Vue 2 vs Vue 3 differences.
The Problem
v-model on a custom Vue component doesn’t update the parent’s value:
<!-- Parent -->
<MyInput v-model="username" />
<p>Username: {{ username }}</p> <!-- Never updates when typing -->Or the value binds in one direction but changes aren’t reflected back:
<!-- Child receives the value but changes don't propagate -->
<input :value="modelValue" @input="$emit('input', $event.target.value)" />
<!-- Vue 3 uses 'update:modelValue' not 'input' -->Or v-model on a component causes a Vue warning:
[Vue warn]: Component emitted event "input" but it is not declared in the emits option.
[Vue warn]: Extraneous non-emits event listeners (update:modelValue) were passed to component
but could not be automatically inherited because component renders fragment or text.Or multiple v-model bindings on the same component don’t work:
<UserForm v-model:firstName="first" v-model:lastName="last" />Why This Happens
v-model is syntactic sugar that expands to a prop + event binding. The event name and prop name changed between Vue 2 and Vue 3:
Vue 2: v-model → :value prop + @input event
<MyInput v-model="username" />
<!-- Expands to: -->
<MyInput :value="username" @input="username = $event" />Vue 3: v-model → :modelValue prop + @update:modelValue event
<MyInput v-model="username" />
<!-- Expands to: -->
<MyInput :modelValue="username" @update:modelValue="username = $event" />Common mistakes:
- Using Vue 2 pattern in Vue 3 — emitting
'input'instead of'update:modelValue' - Using the wrong prop name — prop named
valueinstead ofmodelValuein Vue 3 - Not declaring emits — without
defineEmits, Vue warns and may not propagate events correctly - Missing
defineModel()— Vue 3.4+ has a simpler macro that handles everything automatically - Mutating prop directly — writing to
props.modelValuedirectly instead of emitting an event
Fix 1: Use defineModel (Vue 3.4+)
defineModel() is the simplest modern approach — it creates a reactive ref that automatically syncs with the parent:
<!-- MyInput.vue — using defineModel (Vue 3.4+) -->
<template>
<input
:value="model"
@input="model = $event.target.value"
class="my-input"
/>
</template>
<script setup lang="ts">
// defineModel() creates a two-way binding automatically
// No need for props/emits boilerplate
const model = defineModel<string>({ default: '' });
// model.value reads the current value
// Assigning to model.value emits 'update:modelValue' automatically
</script><!-- Parent usage -->
<script setup>
import { ref } from 'vue';
import MyInput from './MyInput.vue';
const username = ref('');
</script>
<template>
<MyInput v-model="username" />
<p>Username: {{ username }}</p>
</template>Multiple defineModel() bindings:
<!-- UserForm.vue -->
<script setup lang="ts">
const firstName = defineModel<string>('firstName', { default: '' });
const lastName = defineModel<string>('lastName', { default: '' });
</script>
<template>
<input :value="firstName" @input="firstName = $event.target.value" />
<input :value="lastName" @input="lastName = $event.target.value" />
</template><!-- Parent -->
<UserForm v-model:firstName="first" v-model:lastName="last" />Fix 2: Implement v-model Manually (Vue 3, Pre-3.4)
For Vue 3 without defineModel, implement the modelValue prop + update:modelValue emit pattern:
<!-- MyInput.vue — Vue 3 manual pattern -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
class="my-input"
/>
</template>
<script setup lang="ts">
// Define the prop
defineProps<{
modelValue: string;
}>();
// Declare the emit — required in Vue 3
const emit = defineEmits<{
'update:modelValue': [value: string];
}>();
</script>For a checkbox with boolean v-model:
<!-- MyCheckbox.vue -->
<template>
<input
type="checkbox"
:checked="modelValue"
@change="$emit('update:modelValue', $event.target.checked)"
/>
</template>
<script setup lang="ts">
defineProps<{ modelValue: boolean }>();
defineEmits<{ 'update:modelValue': [value: boolean] }>();
</script>Using a computed setter (cleaner for complex components):
<template>
<input v-model="inputValue" />
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{ modelValue: string }>();
const emit = defineEmits<{ 'update:modelValue': [value: string] }>();
// Computed with getter and setter — transparent two-way binding
const inputValue = computed({
get: () => props.modelValue,
set: (value) => emit('update:modelValue', value),
});
// inputValue.value reads props.modelValue
// Assigning to inputValue.value emits the update event
</script>Fix 3: Fix Vue 2 Components (value + input)
Vue 2 uses :value and @input — or the model option to customize:
<!-- Vue 2 — default v-model pattern -->
<template>
<input
:value="value" ← prop named 'value' in Vue 2
@input="$emit('input', $event.target.value)" ← emit 'input' in Vue 2
/>
</template>
<script>
export default {
props: ['value'],
};
</script>Vue 2 model option — customize the prop and event:
<script>
export default {
model: {
prop: 'checked', // Customize prop name (for checkboxes: avoid 'value')
event: 'change', // Customize event name
},
props: {
checked: Boolean,
},
};
</script>Vue 2 .sync modifier (similar to multiple v-model in Vue 3):
<!-- Vue 2 .sync — for multiple two-way bindings -->
<UserForm :firstName.sync="first" :lastName.sync="last" />
<!-- Child emits: this.$emit('update:firstName', newValue) -->Fix 4: Add v-model Modifiers
Vue 3 supports custom modifiers on v-model:
<!-- Parent — using custom modifier 'capitalize' -->
<MyInput v-model.capitalize="username" /><!-- MyInput.vue — handle the modifier -->
<template>
<input
:value="modelValue"
@input="handleInput($event.target.value)"
/>
</template>
<script setup lang="ts">
const props = defineProps<{
modelValue: string;
modelModifiers?: {
capitalize?: boolean; // Corresponds to v-model.capitalize
trim?: boolean;
};
}>();
const emit = defineEmits<{
'update:modelValue': [value: string];
}>();
function handleInput(value: string) {
let result = value;
if (props.modelModifiers?.capitalize) {
result = result.charAt(0).toUpperCase() + result.slice(1);
}
if (props.modelModifiers?.trim) {
result = result.trim();
}
emit('update:modelValue', result);
}
</script>With defineModel() (Vue 3.4+) — even simpler:
<script setup lang="ts">
const [model, modifiers] = defineModel<string>({
set(value) {
if (modifiers.capitalize) {
return value.charAt(0).toUpperCase() + value.slice(1);
}
if (modifiers.trim) {
return value.trim();
}
return value;
},
});
</script>Fix 5: v-model on a Wrapper Component (Forwarding)
When wrapping an <input> element in a component with extra functionality (label, error display), forward v-model correctly:
<!-- FormField.vue — wraps input with label and error -->
<template>
<div class="form-field">
<label :for="fieldId">{{ label }}</label>
<input
:id="fieldId"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
:class="{ error: !!errorMessage }"
v-bind="$attrs" ← Forward other attributes (placeholder, type, etc.)
/>
<span v-if="errorMessage" class="error-msg">{{ errorMessage }}</span>
</div>
</template>
<script setup lang="ts">
import { useId } from 'vue';
defineProps<{
modelValue: string;
label: string;
errorMessage?: string;
}>();
defineEmits<{ 'update:modelValue': [value: string] }>();
const fieldId = useId(); // Unique ID for label/input association
</script>inheritAttrs: false to prevent attribute duplication:
<script setup lang="ts">
// Prevent Vue from automatically applying attrs to the root element
// Use v-bind="$attrs" on the specific element that should receive them
defineOptions({ inheritAttrs: false });
</script>Fix 6: Multiple v-model Bindings
Vue 3 supports multiple v-model bindings on a single component:
<!-- Parent -->
<DateRangePicker
v-model:start="startDate"
v-model:end="endDate"
/><!-- DateRangePicker.vue -->
<template>
<div class="date-range">
<input type="date" :value="start" @input="$emit('update:start', $event.target.value)" />
<span>to</span>
<input type="date" :value="end" @input="$emit('update:end', $event.target.value)" />
</div>
</template>
<script setup lang="ts">
defineProps<{
start: string;
end: string;
}>();
defineEmits<{
'update:start': [value: string];
'update:end': [value: string];
}>();
</script>With defineModel() — even cleaner:
<script setup lang="ts">
const start = defineModel<string>('start');
const end = defineModel<string>('end');
</script>
<template>
<input type="date" v-model="start" />
<span>to</span>
<input type="date" v-model="end" />
</template>Fix 7: Debug v-model Issues
Check the prop and emit in Vue DevTools:
- Open Vue DevTools → select the child component.
- Check Props —
modelValueshould show the current value. - Interact with the input.
- Check Events —
update:modelValueshould appear with the new value.
If update:modelValue doesn’t appear in events, the child isn’t emitting correctly.
Log the emit:
<script setup>
const emit = defineEmits(['update:modelValue']);
function handleInput(event) {
const value = event.target.value;
console.log('Emitting update:modelValue with:', value);
emit('update:modelValue', value);
}
</script>Common emit mistakes:
<!-- WRONG in Vue 3 — emitting 'input' not 'update:modelValue' -->
@input="$emit('input', $event.target.value)"
<!-- WRONG — emitting the Event object instead of the value -->
@input="$emit('update:modelValue', $event)"
<!-- Should be: $event.target.value -->
<!-- WRONG — calling emit in defineEmits instead of using the returned function -->
defineEmits(['update:modelValue'])('update:modelValue', value) // Don't do thisStill Not Working?
Verify Vue version — defineModel() requires Vue 3.4+. For earlier Vue 3 versions, use the manual modelValue + update:modelValue pattern.
v-model on a non-form element — v-model on a <div>, <span>, or custom element requires the component to handle the binding. v-model only works automatically on <input>, <textarea>, <select> for native elements.
$attrs includes onUpdate:modelValue — if your component has inheritAttrs: true (default) and no modelValue prop defined, the event listener may be applied to the root element via $attrs. Add the prop definition to prevent this.
Vuex/Pinia store with v-model — don’t use v-model directly on a store state property (it mutates state directly, bypassing Pinia/Vuex). Use a computed setter:
// With Pinia
const store = useUserStore();
const username = computed({
get: () => store.username,
set: (value) => store.setUsername(value),
});
// Then use v-model="username" — goes through the actionFor related Vue issues, see Fix: Vue Computed Property Not Updating and Fix: Vue Reactive Data Not Updating.
Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.
Was this article helpful?
Related Articles
Fix: Vue Slot Not Working — Named Slots Not Rendering or Scoped Slot Data Not Accessible
How to fix Vue 3 slot issues — v-slot syntax, named slots, scoped slots passing data, default slot content, fallback content, and dynamic slot names.
Fix: Pinia State Not Reactive — Store Changes Not Updating the Component
How to fix Pinia store state not updating components — storeToRefs for destructuring, $patch for partial updates, avoiding reactive() wrapping, getters vs computed, and SSR hydration.
Fix: Vue Computed Property Not Updating — Reactivity Not Triggered
How to fix Vue computed properties not updating — reactive dependency tracking, accessing nested objects, computed setters, watchEffect vs computed, and Vue 3 reactivity pitfalls.
Fix: Vue Router Navigation Guard Not Working — beforeEach and Route Guards
How to fix Vue Router navigation guards not working — beforeEach, beforeEnter, in-component guards, async guards, redirect loops, and route meta authentication patterns.