Form Input Bindings

This section documents the current Vue 3.x release line as published at the official Vue.js documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. The Composition API with <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

v-model creates a two-way binding between a form control and a piece of reactive state: the control shows the state, and editing the control writes it back. It picks the right DOM property and event for each control type. The reference is Form Input Bindings.

The basics

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

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

<template>
  <input v-model="message" placeholder="type here">
  <p>{{ message }}</p>
</template>

v-model ignores the initial value, checked, or selected attribute on the element — the reactive state is the single source of truth, so set the initial value on the ref instead. See Basic Usage.

What v-model desugars to

For a text input, v-model="message" expands to roughly a :value bind plus an @input listener:

<input
  :value="message"
  @input="message = $event.target.value">

Different controls use different pairs — this is why the same directive covers all of them:

Control Bound property Listened event

text, <textarea>

value

input

checkbox, radio

checked

change

<select>

value

change

Knowing the expansion matters when you need something in between: bind :value yourself and add an @input that transforms the text before storing it.

Text and textarea

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

Text interpolation does not work inside <textarea> — use v-model.

Checkbox

See Checkbox. A lone checkbox binds to a boolean:

<script setup>
import { ref } from 'vue'
const agreed = ref(false)
</script>

<template>
  <input type="checkbox" v-model="agreed">
</template>

Several checkboxes that share one array binding collect the value of each checked box:

<script setup>
import { ref } from 'vue'
const picked = ref([])
</script>

<template>
  <input type="checkbox" value="vue" v-model="picked">
  <input type="checkbox" value="svelte" v-model="picked">
  <input type="checkbox" value="solid" v-model="picked">
  <!-- picked.value is e.g. ['vue', 'solid'] -->
</template>

true-value and false-value make a single checkbox store something other than the booleans — and they only affect v-model, not the element’s value attribute. Bind them with :true-value / :false-value for non-string values:

<template>
  <input type="checkbox" v-model="answer" true-value="yes" false-value="no">
  <input type="checkbox" v-model="answer" :true-value="dynamicYes" :false-value="dynamicNo">
</template>

Radio

See Radio.

<script setup>
import { ref } from 'vue'
const size = ref('m')
</script>

<template>
  <input type="radio" value="s" v-model="size"> Small
  <input type="radio" value="m" v-model="size"> Medium
  <!-- :value for a non-string or dynamic value -->
  <input type="radio" :value="{ id: 3 }" v-model="size"> Custom
</template>

Select

See Select.

<template>
  <select v-model="selected">
    <option disabled value="">Please select one</option>
    <option>A</option>
    <option>B</option>
  </select>
</template>

Provide an empty disabled option when the initial value matches no <option> — otherwise iOS renders the select in an unselectable state. For a multiple select, bind to an array:

<template>
  <select v-model="selected" multiple>
    <option>A</option>
    <option>B</option>
    <option>C</option>
  </select>
</template>

Options rendered with v-for use :value, which can hold any type, not just a string — see Value Bindings:

<template>
  <select v-model="selected">
    <option v-for="o in options" :key="o.id" :value="o">{{ o.text }}</option>
  </select>
</template>

Modifiers

See v-model modifiers. They chain, e.g. v-model.trim.lazy.

<template>
  <!-- sync on "change" instead of every "input" -->
  <input v-model.lazy="msg">

  <!-- cast to a number with parseFloat; kept as-is if the result is NaN -->
  <input v-model.number="age">

  <!-- strip leading and trailing whitespace -->
  <input v-model.trim="name">
</template>

<input type="number"> already coerces to a number, so .number is mainly for text inputs that should hold numeric values.

Validation is not built in

Vue’s core has no form-validation layer. For real forms, reach for a dedicated library: VeeValidate wires field-level and whole-form schema validation (Zod, Yup, Valibot) into Vue’s reactivity. The official guide covers only the binding mechanics on this page.

v-model on components

Everything above is v-model on native elements. v-model also works on your own components, backed by defineModel(), with support for arguments and custom modifiers — see Component Events and v-model.

See also