The easiest way to work with forms in JavaScript.
1What is Two-Way Binding?
Normally, data flows one way: from your JavaScript state into the HTML. Two-way binding means that if the HTML changes (e.g., a user types in an input), the JavaScript state updates automatically. And if the JavaScript state changes, the HTML updates.
2The `v-model` Directive
v-model is the magic directive that implements two-way binding on form input, textarea, and select elements. It automatically picks the correct way to update the element based on the input type.
3v-model Modifiers
Vue provides built-in modifiers: .lazy (syncs after 'change' event instead of 'input'), .number (typecasts input as a number), and .trim (strips whitespace).
4Step-by-Step Breakdown
Forms are a huge part of web development. In vanilla JS, syncing an input with a variable requires listening to the "input" event and manually updating the variable.
Vue makes this trivial with Two-Way Data Binding using the v-model directive.
Which directive is used in Vue to create two-way data bindings on form inputs?
- →v-bind
- →v-model
- →v-on
Why "Two-Way"?
1. If the user types, the message ref updates automatically.
2. If you change message.value in JS, the input field updates automatically.
v-model works magically on almost all form elements: text inputs, checkboxes, radio buttons, and select dropdowns.
If you use v-model on a <input type="checkbox">, what data type should the bound ref typically hold?
- →String
- →Number
- →Boolean
Vue also provides modifiers for v-model. For example, v-model.number automatically converts the user's input from a string to a Number.
Another modifier is v-model.trim, which automatically strips whitespace from the beginning and end of the input.
Which v-model modifier automatically removes whitespace from the beginning and end of the user input?
- →strip
- →trim
- →clean
Behind the scenes, v-model is just syntactic sugar for binding the value prop and listening to the input event. But it saves us writing so much code!
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1v-model Doesn't Replace an Explicit Label
v-model wires an input's value to reactive state, but it does nothing for the input's accessible name — a text field bound with v-model="email" still needs an associated <label for="email"> or aria-label, since screen reader users have no way to know what the field is for based on its reactive binding alone.
<label for="email">Email</label>
<input id="email" v-model="email" />SEO Implications
- 1
v-model State Lives in JavaScript Memory, Not in the Rendered HTML Value Attribute
During SSR, an <input v-model="query"> renders its current value into the HTML's value attribute, so a pre-filled search box is visible to a crawler fetching the raw page — but any value the user types afterward exists purely in the browser's reactive state and is never reflected back into a server-rendered document.
Best Practices
Use .lazy, .number, and .trim Instead of Manual Event Handling
Rather than writing @input="val => data = Number(val.target.value.trim())", express the same intent declaratively with v-model.lazy.number or v-model.trim — the modifiers keep the template readable and avoid re-implementing type coercion Vue already provides.
Frequent Bugs
Using v-model on a custom component without implementing the modelValue prop/event pair.
Plain v-model="x" on a custom component expects that component to accept a modelValue prop and emit an update:modelValue event; if the component uses a different prop name with no matching emit, the parent's v-model binding silently does nothing — the child never receives updates or reports its own changes back.
Real-World Examples
A Custom Currency Input Component
A <CurrencyInput v-model="price" /> wraps a native input, formats the displayed value with commas, but emits the raw numeric value via update:modelValue, so the parent's price ref always stays a clean number even though the visible text shows '$1,250.00'.
// CurrencyInput.vue
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
function onInput(e) {
emit('update:modelValue', Number(e.target.value.replace(/[^0-9.]/g, '')));
}