Home / Blog / v-model & defineModel: Two-Way Binding in Vue 3
v-model & defineModel: Two-Way Binding in Vue 3

v-model & defineModel: Two-Way Binding in Vue 3

Two-way binding is one of the first things you reach for in Vue, and Vue 3 hands you two tools for it. v-model is the directive you put on form inputs, and defineModel is the macro that teaches your own components to speak v-model. This guide covers both, starting with v-model on every native input and its modifiers, then defineModel with default values, named models, and TypeScript.

In short, v-model is Vue's directive for two-way data binding. It keeps a piece of state and a form input (or a child component) in sync, updating your data when the user types and updating the input when your data changes. defineModel is a Vue 3.4+ compiler macro you call inside a child component's <script setup> so that component works with v-model, and it replaces the older modelValue prop plus update:modelValue event pattern with a single writable ref.

v-model is a feature you reach for constantly, so it is a good place to start if Vue is new to you. Our free, interactive Learn Vue tutorial walks through it with runnable examples you edit in the browser and watch react, and this guide picks up from there and takes two-way binding all the way to custom components and defineModel.

What two-way binding actually means

Two-way binding is a live link between a component's template and its data. When the user does something in the UI, like typing in a field or ticking a checkbox, the data updates on its own.

It runs the other direction too. Change the data in code and the template redraws with the new value right away. That is the whole appeal, one source of truth that your state and the UI both point at.

In Vue 3 with the Composition API, the data side of that link is a ref, and v-model is the directive that wires it to an input.

What is v-model in Vue 3?

v-model creates two-way binding between reactive state and a form input or a component. On a native input it is shorthand for two things you would otherwise write yourself, a :value binding and an @input handler, so a single directive replaces both.

Say a parent App.vue has a form and you want every edit to land in a variable called inputValue, and any change to inputValue in code to show up in the field. That back-and-forth is two-way binding, and here it is in <script setup>.

<template>
  <form action="/">
    <input v-model="inputValue" />
    <p>You typed: {{ inputValue }}</p>
  </form>
</template>

<script setup>
  import { ref } from 'vue'

  const inputValue = ref('')
</script>

That one v-model stands in for a manual :value="inputValue" plus an @input handler. Everything you type flows into inputValue, and any change to inputValue flows back into the field.

How to use v-model on native inputs

v-model works on more than text boxes. It picks the right property and event for each kind of control on its own, so the same directive covers a whole form.

Text inputs and textarea

<template>
  <input v-model="name" />
  <textarea v-model="bio"></textarea>
</template>

<script setup>
  import { ref } from 'vue'

  const name = ref('')
  const bio = ref('')
</script>

Checkbox, single boolean and multiple values

A single checkbox binds to a boolean.

<template>
  <input type="checkbox" v-model="subscribed" />
  <span>{{ subscribed ? 'Subscribed' : 'Not subscribed' }}</span>
</template>

<script setup>
  import { ref } from 'vue'

  const subscribed = ref(false)
</script>

Point several checkboxes at one v-model and Vue binds them to an array of the checked values.

<template>
  <label><input type="checkbox" value="cheese" v-model="toppings" /> Cheese</label>
  <label><input type="checkbox" value="mushroom" v-model="toppings" /> Mushroom</label>
  <label><input type="checkbox" value="onion" v-model="toppings" /> Onion</label>
  <p>Selected: {{ toppings }}</p>
</template>

<script setup>
  import { ref } from 'vue'

  const toppings = ref([])
</script>

Radio buttons

Radio buttons on the same v-model bind to the value of whichever one is selected.

<template>
  <label><input type="radio" value="free" v-model="plan" /> Free</label>
  <label><input type="radio" value="pro" v-model="plan" /> Pro</label>
  <p>Chosen plan: {{ plan }}</p>
</template>

<script setup>
  import { ref } from 'vue'

  const plan = ref('free')
</script>

Select, single and multiple

A single select binds to the value of the chosen <option>. Add multiple and v-model binds to an array instead.

<template>
  <!-- single select -->
  <select v-model="plan">
    <option value="free">Free</option>
    <option value="pro">Pro</option>
  </select>

  <!-- multiple select binds to an array -->
  <select v-model="features" multiple>
    <option value="ssr">SSR</option>
    <option value="pwa">PWA</option>
    <option value="i18n">i18n</option>
  </select>
</template>

<script setup>
  import { ref } from 'vue'

  const plan = ref('free')
  const features = ref([])
</script>

Native input cheatsheet

Form control What v-model binds Value type
Text input / textarea value plus the input event string
Checkbox (single) checked state boolean
Checkbox (multiple, shared v-model) list of checked values array
Radio buttons (shared v-model) value of the selected radio string / any
Select (single) value of the chosen option string / any
Select with multiple values of the chosen options array

v-model modifiers, .trim, .number, and .lazy

Modifiers change what v-model stores without any extra handler code.

  • .trim strips leading and trailing whitespace.
  • .number casts the value to a number.
  • .lazy syncs on the change event instead of on every keystroke.
<template>
  <input v-model.trim="email" />
  <input v-model.number="age" />
  <input v-model.lazy="message" />
</template>

<script setup>
  import { ref } from 'vue'

  const email = ref('')
  const age = ref(0)
  const message = ref('')
</script>

.number earns its keep more than it looks, because an <input> hands you a string every time, so without it age is the string '30', not the number 30, and the first bit of math you do goes sideways. You can chain modifiers too, for example v-model.trim.lazy="username".

How to use v-model on a custom component

Native inputs are the easy case. The interesting one is putting two-way binding on your own Single-File Component. This next pattern is the one every Vue 3 version supports, and you will still read it in plenty of codebases, so it is worth knowing even after you move to defineModel.

Say App.vue uses a custom FormInput and wants any change inside it reflected in inputValue.

<template>
  <FormInput v-model="inputValue" />
</template>

<script setup>
  import { ref } from 'vue'
  import FormInput from './components/FormInput.vue'

  const inputValue = ref('')
</script>

Put v-model on a component and Vue passes the value down as a prop named modelValue and waits for an event named update:modelValue back up. So inside FormInput you declare the prop, bind it to the input, and emit that event on every keystroke.

<template>
  <input type="text" :value="modelValue" @input="updateValue" />
</template>

<script setup>
  defineProps(['modelValue'])
  const emit = defineEmits(['update:modelValue'])

  const updateValue = (e) => {
    emit('update:modelValue', e.target.value)
  }
</script>

That update:modelValue is the exact event v-model on the parent listens for, which is why inputValue updates the moment the child emits. If you ever wire the parent up by hand instead of using v-model, you would listen with @update:modelValue, and Vue accepts the kebab-case @update:model-value too. Either way, that is the full two-way loop, written out by hand.

What is defineModel in Vue 3?

defineModel is a compiler macro that folds the whole modelValue prop plus update:modelValue emit into one line. It shipped as experimental in Vue 3.3 and became stable in 3.4. It returns a writable ref, so you read it to get the current value and assign to it (or bind it with v-model) to send a new value back to the parent.

Here is that same FormInput, rewritten.

<template>
  <input type="text" v-model="model" />
</template>

<script setup>
  const model = defineModel()
</script>

That is the whole component. defineModel declares the prop, wires the emit, and hands you a ref you can bind with v-model directly. The manual version does the same job, it is just three moving parts you keep in sync yourself, and defineModel handles all three for you. In a Nuxt project it behaves the same way, since Nuxt runs the same Vue compiler.

defineModel default value, required, and type

defineModel takes the same options you would give a prop.

<template>
  <input type="text" v-model="model" />
</template>

<script setup>
  const model = defineModel({ default: '' })
</script>

If the parent leaves the v-model off, model falls back to that default. Two more options carry over from normal props. defineModel({ type: Number, default: 0 }) types the value and gives it a numeric fallback, and defineModel({ required: true }) forces the parent to bind a v-model or Vue warns you in the console.

Named models and multiple v-models on one component

By default v-model maps to a single model named modelValue. Give defineModel a name and you get a named model, which lets one component expose several independent v-model bindings. Name each one in the child.

<!-- UserName.vue -->
<template>
  <input type="text" v-model="firstName" />
  <input type="text" v-model="lastName" />
</template>

<script setup>
  const firstName = defineModel('firstName')
  const lastName = defineModel('lastName')
</script>

Then bind them by name in the parent.

<template>
  <UserName v-model:first-name="first" v-model:last-name="last" />
</template>

<script setup>
  import { ref } from 'vue'
  import UserName from './components/UserName.vue'

  const first = ref('')
  const last = ref('')
</script>

Each named model gets its own prop and its own update: event, so v-model:first-name and v-model:last-name never step on each other.

defineModel with modifiers

defineModel can also read and transform the modifiers a parent adds to its v-model. Destructure the second value it returns to get the modifiers, then use a set transformer to change the value on the way out.

<template>
  <input type="text" v-model="model" />
</template>

<script setup>
  const [model, modifiers] = defineModel({
    set(value) {
      if (modifiers.capitalize) {
        return value.charAt(0).toUpperCase() + value.slice(1)
      }
      return value
    }
  })
</script>

Now a parent can write <MyInput v-model.capitalize="text" /> and your component decides what .capitalize does, the same way .trim and .number work on native inputs.

defineModel with TypeScript

defineModel is typed. Pass a type argument and the ref comes back inferred, with no separate prop interface to maintain.

<script setup lang="ts">
  const model = defineModel<string>()
  const count = defineModel<number>('count', { default: 0 })
</script>

v-model vs defineModel, what is the difference?

The "v-model vs defineModel" framing is a bit of a trap, because they are two halves of the same connection. v-model is what the parent writes, and defineModel is how the child receives it.

v-model (directive) defineModel (macro)
Where it lives In a template, on an input or a component Inside a child's <script setup>
What it is A directive for two-way binding A macro that returns a writable ref
Its job Passes a value down and listens for updates Declares the prop and emit that v-model talks to
Native inputs Works with no setup Not needed, use v-model on the input
Custom components The parent-side syntax The child-side helper (Vue 3.4+)
Before Vue 3.4 Same syntax Use a modelValue prop plus update:modelValue emit

So use v-model anywhere you consume a value, and reach for defineModel inside a component when you want it to be v-model-friendly without hand-writing the prop and emit.

Frequently asked questions

What is defineModel in Vue 3?
defineModel is a compiler macro, stable since Vue 3.4, that you call inside a child component's <script setup>. It returns a writable ref and declares the modelValue prop and update:modelValue event for you, so the component works with v-model in one line instead of a manual prop plus emit.

What is the difference between v-model and defineModel?
v-model is the directive you put in a template to create two-way binding. defineModel is the macro you call inside a custom component to make it answer to v-model. v-model is the parent side, defineModel is the child side.

How do I use v-model on a custom component?
Put v-model="state" on the component in the parent. In the child, call const model = defineModel() and bind it with v-model (Vue 3.4+), or on older versions declare a modelValue prop and emit update:modelValue yourself.

Can I have multiple v-models on one component?
Yes. Name each model with defineModel('firstName') and defineModel('lastName'), then bind them in the parent with v-model:first-name and v-model:last-name. Each named model is independent.

How do I set a default value with defineModel?
Pass a default option, like const model = defineModel({ default: '' }). If the parent does not bind a v-model, the model uses that default.

Wrapping up

v-model covers two-way binding on every native input, from text fields to checkboxes, radios, and selects, with .trim, .number, and .lazy to shape the value before it lands in your state. defineModel gives your own components that same v-model experience from the inside in a single line, with default values, named models, modifiers, and TypeScript along for the ride.

Learn both and two-way binding turns from boilerplate into a one-liner you stop thinking about. If you want to see it inside a full app, the Vue.js Master Class builds one end to end, and you can start editing reactive state right now in our free Learn Vue tutorial. The complete code for this article is on StackBlitz if you want to poke at it.

Start learning Vue.js for free

Comments

Latest Vue School Articles

5 Component Design Patterns to Boost Your Vue.js Applications

5 Component Design Patterns to Boost Your Vue.js Applications

5 essential Vue.js component design patterns, including branching components, slots usage, list organization, smart vs dumb components, and form handling - perfect for both Vue beginners and experienced developers looking to improve code maintainability and scalability.
Vibe Coding a Collaborative Editor with Comment Support with Nuxt UI and Jazz

Vibe Coding a Collaborative Editor with Comment Support with Nuxt UI and Jazz

Why I built a Nuxt + Jazz powered real time editor, how you can use it, and a list of takeaways on building with the help of AI.
VueSchool logo

Our goal is to be the number one source of Vue.js knowledge for all skill levels. We offer the knowledge of our industry leaders through awesome video courses for a ridiculously low price.

More than 250.000 users have already joined us. You are welcome too!

Follow us on Social