Skip to content

Fix: Vue v-model Not Working on Custom Components — Prop Not Syncing

FixDevs ·

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 value instead of modelValue in 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.modelValue directly 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:

  1. Open Vue DevTools → select the child component.
  2. Check PropsmodelValue should show the current value.
  3. Interact with the input.
  4. Check Eventsupdate:modelValue should 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 this

Still Not Working?

Verify Vue versiondefineModel() requires Vue 3.4+. For earlier Vue 3 versions, use the manual modelValue + update:modelValue pattern.

v-model on a non-form elementv-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 action

For related Vue issues, see Fix: Vue Computed Property Not Updating and Fix: Vue Reactive Data Not Updating.

F

FixDevs

Solo developer based in Japan. Every solution is cross-referenced with official documentation and tested before publishing.

Was this article helpful?

Related Articles