Form Input Bindings with v-model
v-model provides two-way data binding on form inputs and on custom components. On a native <input>, <textarea>, or <select>, it is syntactic sugar that combines a :value binding with an @input (or @change, for checkboxes/radios/selects) listener that updates the bound ref. Rather than manually wiring both directions yourself, v-model="someRef" handles reading and writing in one directive. Crucially, v-model is not magic — it desugars to explicit prop-and-event wiring, and understanding that expansion is what makes v-model on custom components make sense.
Cricket analogy: v-model on a scorecard's run-entry field is like a scorer who both reads the current total and writes new runs in one motion, rather than a separate reader and a separate writer needing to be manually kept in sync on every ball.
What v-model compiles to
On a plain text input, <input v-model="searchTerm" /> expands to <input :value="searchTerm" @input="searchTerm = $event.target.value" />. This is why v-model requires a ref (or a reactive property) rather than a plain variable — the binding needs something it can reassign on every input event. On a checkbox, the desugaring uses :checked and toggles a boolean, or pushes/removes a value from an array if v-model is bound to an array of values across multiple checkboxes. On <select>, it binds :value and listens for change. Understanding this expansion also explains why v-model doesn't work on a destructured reactive property or a plain object field passed by value — the binding must resolve to something assignable.
Cricket analogy: Typing runs into a text field desugars to reading the value and writing it back on every keystroke, which is why v-model needs a ref it can reassign; on a wicket checkbox it toggles a boolean, and on a dismissal-type select it binds value and listens for change.
v-model on custom components
On a custom component, v-model="value" by default expands to a modelValue prop and an update:modelValue emitted event. The child component must declare defineProps(['modelValue']) and call emit('update:modelValue', newValue) whenever the value changes internally. You can also bind multiple v-models on the same component with named arguments — v-model:title="pageTitle" expands to a title prop and an update:title event — which lets a single component expose several independently two-way-bound fields.
Cricket analogy: v-model on a custom RunCounter component expands by default to a modelValue prop and an update:modelValue emit, so the child must declare the prop and emit the new count; a named v-model:overs lets the same component also expose overs as an independent two-way field.
<!-- CurrencyInput.vue: a custom component supporting v-model -->
<script setup>
const props = defineProps({
modelValue: { type: Number, required: true }
})
const emit = defineEmits(['update:modelValue'])
function onInput(event) {
const parsed = parseFloat(event.target.value)
emit('update:modelValue', Number.isNaN(parsed) ? 0 : parsed)
}
</script>
<template>
<input
type="number"
:value="modelValue"
@input="onInput"
/>
</template>
<!-- Parent.vue -->
<script setup>
import { ref } from 'vue'
import CurrencyInput from './CurrencyInput.vue'
const price = ref(19.99)
</script>
<template>
<CurrencyInput v-model="price" />
<p>Current price: {{ price }}</p>
</template>React has no built-in equivalent to v-model — two-way binding is always assembled manually with value={state} and onChange={e => setState(e.target.value)}. Vue's v-model is essentially that exact pattern with a directive that generates the boilerplate for you, and on custom components it standardizes the prop/event naming convention (modelValue / update:modelValue) so any two components adopting the convention interoperate predictably.
A common mistake is trying to v-model directly on a prop inside a child component (v-model="props.someProp"), which either throws a warning or silently fails, because props are meant to flow one-way from parent to child — mutating a prop directly violates Vue's one-way data flow and will be overwritten the next time the parent re-renders. The correct pattern is exactly the modelValue/update:modelValue (or a named v-model) convention shown above: the child never mutates the prop, it only emits an event asking the parent to update the source of truth.
- v-model is sugar for a
:valuebinding plus an event listener (@input, @change) that reassigns the bound ref. - On custom components, v-model defaults to a modelValue prop and an update:modelValue emitted event.
- Named v-models (
v-model:title) expand to atitleprop andupdate:titleevent, enabling multiple independent bindings on one component. - v-model requires an assignable target (a ref or reactive property) — it cannot bind to a destructured value or a non-reactive plain variable.
- Child components must never mutate a prop directly to implement v-model — they emit an update event and let the parent own the source of truth.
- The exact DOM event v-model listens for depends on the element type: input for text fields, change for checkboxes/radios/selects.
Practice what you learned
1. What does `v-model="searchTerm"` on a text `<input>` expand to?
2. By default, what prop and event does v-model use on a custom component?
3. What does `v-model:title="pageTitle"` expand to on a custom component?
4. Why can't v-model bind directly to a prop inside a child component?
5. Which native DOM event does v-model typically listen to for a checkbox input?
Was this page helpful?
You May Also Like
Event Handling with v-on
Explains how Vue's v-on directive (shorthand @) wires DOM and custom component events to handlers, including inline expressions, method handlers, and modifiers.
reactive() and ref()
A deep dive into Vue 3's two core reactivity primitives — ref() for any value type and reactive() for objects — and when to use each.
Props Explained
Explains how props pass data from parent to child components in Vue, covering declaration, validation, defaults, and the one-way data flow rule.
Emitting Custom Events
Learn how child components communicate upward to parents using Vue's custom event system, including typed emits and the defineEmits macro.
Related Reading
Related Study Notes in Web Development
Browse all study notesWebSockets Study Notes
Web Development · 30 topics
Web DevelopmentWebAssembly Study Notes
WebAssembly · 30 topics
Web DevelopmentgRPC Study Notes
Protocol Buffers · 30 topics
Web DevelopmentSpring Boot Study Notes
Java · 30 topics
Web DevelopmentFlask Study Notes
Python · 30 topics
Web DevelopmentDjango Study Notes
Python · 30 topics