Explain why v-model is considered syntax sugar in Vue: it’s compiled into a prop binding + an event listener. Show what it expands to for native form elements (text input, checkbox, select) and for custom components in Vue 3 (modelValue + update:modelValue). Include how v-model arguments (v-model:foo) and common modifiers (.trim, .number, .lazy) change the expansion.
Why is v-model syntax sugar, and what does it expand to?
Use guided tracks for structured prep, then practice company-specific question sets when you want targeted interview coverage.
Core ideav-model is syntax sugar because it doesn’t add a new runtime “two-way binding engine”. Vue’s template compiler rewrites it into:
1) a prop/attribute binding (state → DOM/component)
2) an event listener (DOM/component → state)
So it’s just a shorter way to write “bind value + update on event”.
Usage | Expands to (conceptually) | Notes |
|---|---|---|
|
| Text-like inputs use |
|
| Checkbox binds |
|
| Select updates on |
|
| Custom components use |
|
| Argument changes prop/event name. |
Native input example (the “long form”)
<template>
<!-- Sugar -->
<input v-model="msg" />
<!-- Desugared -->
<input
:value="msg"
@input="msg = $event.target.value"
/>
</template>
<script setup>
import { ref } from 'vue';
const msg = ref('hello');
</script>
Checkbox + select (different DOM properties/events)
People call v-model “smart sugar” because it chooses the correct DOM property and event per form control.
<template>
<!-- Checkbox: checked + change -->
<input type="checkbox" v-model="ok" />
<input
type="checkbox"
:checked="ok"
@change="ok = $event.target.checked"
/>
<!-- Select: value + change -->
<select v-model="pick">
<option value="a">A</option>
<option value="b">B</option>
</select>
<select
:value="pick"
@change="pick = $event.target.value"
>
<option value="a">A</option>
<option value="b">B</option>
</select>
</template>
<script setup>
import { ref } from 'vue';
const ok = ref(false);
const pick = ref('a');
</script>
Custom components (Vue 3): modelValue + update:modelValue<br>For components, v-model becomes a normal prop + a normal emitted event. The parent “listens” and assigns.
<!-- Parent.vue -->
<template>
<!-- Sugar -->
<UserNameInput v-model="name" />
<!-- Desugared -->
<UserNameInput
:modelValue="name"
@update:modelValue="name = $event"
/>
</template>
<script setup>
import { ref } from 'vue';
import UserNameInput from './UserNameInput.vue';
const name = ref('Müslüm');
</script>
<!-- UserNameInput.vue -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps({
modelValue: { type: String, required: true }
});
defineEmits(['update:modelValue']);
</script>
v-model arguments (Vue 3): v-model:foo
Arguments just swap the prop/event names. This enables multiple v-model bindings on one component.
<!-- Parent.vue -->
<template>
<RangePicker v-model:start="start" v-model:end="end" />
</template>
<script setup>
import { ref } from 'vue';
import RangePicker from './RangePicker.vue';
const start = ref('2025-01-01');
const end = ref('2025-12-31');
</script>
<!-- RangePicker.vue -->
<template>
<input :value="start" @input="$emit('update:start', $event.target.value)" />
<input :value="end" @input="$emit('update:end', $event.target.value)" />
</template>
<script setup>
defineProps({
start: String,
end: String
});
defineEmits(['update:start', 'update:end']);
</script>
Modifiers (.trim, .number, .lazy) change the update expression
Modifiers don’t change the “prop + event” structure, they change how the incoming value is read/coerced and when updates happen.
Modifier | Effect | Desugared idea |
|---|---|---|
| Trims whitespace |
|
| Coerces to number |
|
| Updates on change/blur-like timing instead of input | Uses |
Interview-ready takeawayv-model is compile-time sugar for “bind current value + listen and assign updates”. For native inputs it picks the correct DOM property/event. For components in Vue 3 it maps to modelValue + update:modelValue (or v-model:arg → arg + update:arg).