---
title: "Forms and inputs: v-model modifiers, checkboxes and selects · vuejs.school"
description: "How v-model behaves on text fields, checkboxes, radios and selects, what .lazy, .number and .trim do, and why your numbers sometimes arrive as strings."
canonical_url: "https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects"
meta:
  "og:description": "How v-model behaves on text fields, checkboxes, radios and selects, what .lazy, .number and .trim do, and why your numbers sometimes arrive as strings."
  "og:image": "https://vuejs.school/_og/d/a_~Rm9ybXMgYW5kIGlucHV0czogdi1tb2RlbCBtb2RpZmllcnMsIGNoZWNrYm94ZXMgYW5kIHNlbGVjdHMgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~Rm9ybXMgYW5kIGlucHV0czogdi1tb2RlbCBtb2RpZmllcnMsIGNoZWNrYm94ZXMgYW5kIHNlbGVjdHM%2Cdescription_~SG93IHYtbW9kZWwgYmVoYXZlcyBvbiB0ZXh0IGZpZWxkcywgY2hlY2tib3hlcywgcmFkaW9zIGFuZCBzZWxlY3RzLCB3aGF0IC5sYXp5LCAubnVtYmVyIGFuZCAudHJpbSBkbywgYW5kIHdoeSB5b3VyIG51bWJlcnMgc29tZXRpbWVzIGFycml2ZSBhcyBzdHJpbmdzLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9mb3Jtcy1hbmQtaW5wdXRzLXYtbW9kZWwtbW9kaWZpZXJzLWNoZWNrYm94ZXMtYW5kLXNlbGVjdHMi%2Cs_b7kmPSi4ablkBpkY.png"
  "og:image:alt": "Forms and inputs: v-model modifiers, checkboxes and selects · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "Forms and inputs: v-model modifiers, checkboxes and selects · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~Rm9ybXMgYW5kIGlucHV0czogdi1tb2RlbCBtb2RpZmllcnMsIGNoZWNrYm94ZXMgYW5kIHNlbGVjdHMgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~Rm9ybXMgYW5kIGlucHV0czogdi1tb2RlbCBtb2RpZmllcnMsIGNoZWNrYm94ZXMgYW5kIHNlbGVjdHM%2Cdescription_~SG93IHYtbW9kZWwgYmVoYXZlcyBvbiB0ZXh0IGZpZWxkcywgY2hlY2tib3hlcywgcmFkaW9zIGFuZCBzZWxlY3RzLCB3aGF0IC5sYXp5LCAubnVtYmVyIGFuZCAudHJpbSBkbywgYW5kIHdoeSB5b3VyIG51bWJlcnMgc29tZXRpbWVzIGFycml2ZSBhcyBzdHJpbmdzLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9mb3Jtcy1hbmQtaW5wdXRzLXYtbW9kZWwtbW9kaWZpZXJzLWNoZWNrYm94ZXMtYW5kLXNlbGVjdHMi%2Cs_b7kmPSi4ablkBpkY.png"
  "twitter:image:alt": "Forms and inputs: v-model modifiers, checkboxes and selects · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Text fields and textareas

On a text `<input>` or a `<textarea>`, `v-model` reads the element's `value` and updates your state on every `input` event, which means on every keystroke:

*src/components/Greeting.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

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

<template>
  <label for="name">Name</label>
  <input id="name" v-model="name">
  <p>Hello, {{ name || 'stranger' }}.</p>

  <label for="notes">Notes</label>
  <textarea id="notes" v-model="notes"></textarea>
</template>
```

Two rules from the [Vue guide](https://vuejs.org/guide/essentials/forms.html) save confusion:

- `v-model` ignores a starting `value`, `checked` or `selected` attribute written in the template. Your state is the source of truth, so put the starting value in the `ref`. (On radio buttons, options and checkboxes bound to a list, `value` still matters: it is what the field writes when chosen.)
- `{{ notes }}` between `<textarea>` tags only works one way. It shows the text, but nothing the user types reaches `notes`. Use `v-model`.

## Checkboxes: one boolean or a list

A single checkbox bound to a boolean gives you `true` or `false`. Several checkboxes bound to the same array give you a list of the `value`s that are ticked. If you want a single checkbox to produce something other than a boolean, `true-value` and `false-value` set what it writes:

*src/components/Preferences.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

const agree = ref(false)
const topics = ref<string[]>([])
const newsletter = ref<'weekly' | 'never'>('never')
</script>

<template>
  <label><input v-model="agree" type="checkbox"> I accept the terms</label>

  <fieldset>
    <legend>Topics you want covered</legend>
    <label><input v-model="topics" type="checkbox" value="components"> Components</label>
    <label><input v-model="topics" type="checkbox" value="routing"> Routing</label>
    <label><input v-model="topics" type="checkbox" value="testing"> Testing</label>
  </fieldset>

  <label>
    <input v-model="newsletter" type="checkbox" true-value="weekly" false-value="never">
    Send me the weekly newsletter
  </label>
</template>
```

Tick Testing, then Routing, and `topics` is `['testing', 'routing']`: the order you clicked, not the page order. Ticking the last box sets `newsletter` to `'weekly'`, and unticking it sets `'never'`.

`true-value` and `false-value` are Vue attributes that work only with `v-model`. They change your state, not the form data: a classic form post would still send the box's own `value`, or nothing when it is unticked.

The list version works only when the starting value is an array (or a `Set`). Start with a string instead and Vue falls back to the single-checkbox behaviour:

*src/components/Preferences.vue**Breaks***

```
<script setup lang="ts">
import { ref } from 'vue'

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

<template>
  <label><input v-model="topics" type="checkbox" value="components"> Components</label>
  <label><input v-model="topics" type="checkbox" value="routing"> Routing</label>
</template>
```

Click Components and `topics` becomes `true`. Every box bound to it now compares itself to `true`, so they all tick at once. TypeScript does not catch it either: `ref('')` says string, and `vue-tsc` accepts the binding. The fix is one line:

*src/components/Preferences.vue**Works***

```
const topics = ref<string[]>([])
```

## Radio buttons and selects: strings, objects and arrays

Radio buttons and selects both write the value of what the user picked. This component has a radio group, a select of objects and a `multiple` select:

*src/components/SessionPicker.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

type Session = { id: string; label: string; starts: string }

const morning: Session = { id: 'am', label: 'Morning', starts: '10:00' }
const afternoon: Session = { id: 'pm', label: 'Afternoon', starts: '14:00' }
const sessions = [morning, afternoon]

const size = ref<'S' | 'M' | 'L'>('M')
const session = ref(morning)
const diets = ref<string[]>([])
</script>

<template>
  <fieldset>
    <legend>T-shirt size</legend>
    <label><input v-model="size" type="radio" name="size" value="S"> Small</label>
    <label><input v-model="size" type="radio" name="size" value="M"> Medium</label>
    <label><input v-model="size" type="radio" name="size" value="L"> Large</label>
  </fieldset>

  <label for="session">Session</label>
  <select id="session" v-model="session">
    <option v-for="s in sessions" :key="s.id" :value="s">{{ s.label }} ({{ s.starts }})</option>
  </select>

  <label for="diets">Dietary needs</label>
  <select id="diets" v-model="diets" multiple>
    <option value="vegetarian">Vegetarian</option>
    <option value="vegan">Vegan</option>
    <option value="gluten-free">Gluten-free</option>
  </select>
</template>
```

- **The radio buttons** share one piece of state, and each writes its own `value` when chosen: pick Large and `size` is `'L'`. Give them the same `name` as well, so the browser treats them as one group for the keyboard.
- **The session select** binds each option with `:value="s"`, so `session` holds the whole object, not a string id. That saves a lookup later. Vue compares option values by their contents, so a fresh object with the same fields still selects the right option.
- **The `multiple` select** writes an array of the chosen values, so `diets` starts as one. Bind it to anything but an array or a `Set` and Vue warns in the console: `<select multiple v-model> expects an Array or Set value for its binding, but got String.`

If a single select starts with a value that matches no option, it shows nothing selected. The guide recommends a first option such as `<option disabled value="">Please select one</option>`, because iOS does not fire a change event when the user picks the first item from that state.

## Modifiers: .lazy, .trim and .number

Modifiers go after a dot and change how `v-model` copies the value:

*src/components/SignupFields.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

const nickname = ref('')
const email = ref('')
const age = ref<number | string>('')
</script>

<template>
  <label for="nickname">Nickname</label>
  <input id="nickname" v-model.lazy="nickname">
  <label for="email">Email</label>
  <input id="email" v-model.trim="email" type="email">
  <label for="age">Age</label>
  <input id="age" v-model.number="age" inputmode="numeric">
</template>
```

- `.lazy` updates the state on `change` instead of `input`: when the field loses focus or the user presses Enter, not on every key. Use it when each update triggers something slow, such as a search request or a heavy check.
- `.trim` strips spaces from both ends. When the field loses focus, Vue also writes the trimmed text back into it, so the user sees what was kept.
- `.number` converts the text with `parseFloat()`. If that fails, you get the original string. An empty field gives `''`, not `0`. So `age` is typed `number | string`: `inputmode="numeric"` only asks phones for a number keypad, and typed letters stay a string.

Inputs with `type="number"` get `.number` automatically. Modifiers combine, so `v-model.lazy.trim` is fine.

## Fix numbers that arrive as strings

Text fields and options hand Vue text unless you say otherwise. Here a booking form counts people from two selects:

*src/components/Guests.vue**Breaks***

```
<script setup lang="ts">
import { computed, ref } from 'vue'

const adults = ref(1)
const children = ref(0)
const people = computed(() => adults.value + children.value)
</script>

<template>
  <label for="adults">Adults</label>
  <select id="adults" v-model="adults">
    <option v-for="n in 4" :key="n">{{ n }}</option>
  </select>
  <label for="children">Children</label>
  <select id="children" v-model="children">
    <option v-for="n in 4" :key="n">{{ n - 1 }}</option>
  </select>
  <p>Total guests: {{ people }}</p>
</template>
```

The page starts with a total of 1, which looks right. Choose 2 adults and the total becomes 20. An option without a `value` attribute uses its text, and text is a string, so `adults` becomes `'2'`, and `'2' + 0` joins the two into `'20'`. Pick one child and you get `'21'`. As before, `ref(1)` claims to be a number, and `vue-tsc` does not flag the string that `v-model` writes into it.

Keep the script and bind `:value` in the template, so each option carries a real number:

*src/components/Guests.vue**Works***

```
<template>
  <label for="adults">Adults</label>
  <select id="adults" v-model="adults">
    <option v-for="n in 4" :key="n" :value="n">{{ n }}</option>
  </select>
  <label for="children">Children</label>
  <select id="children" v-model="children">
    <option v-for="n in 4" :key="n" :value="n - 1">{{ n - 1 }}</option>
  </select>
  <p>Total guests: {{ people }}</p>
</template>
```

Now 2 adults and 1 child make 3. `v-model.number` on each select would also work, but binding `:value` is the better habit, because it is the same move that lets an option hold an object.

A `type="number"` input writes a number or `''`: the browser reports a cleared field, or anything that is not a valid number, as empty. Type it as `ref<number | ''>(1)` and check before you use it, as the form below does.

## Submit the form without reloading the page

A `<form>` with a submit button already handles the Enter key and runs the browser's own checks for `required` and `type="email"`. Left alone, the browser then loads the form's `action` address, or reloads the page when there is none. `@submit.prevent` calls `event.preventDefault()` for you, so your handler runs and the page stays put:

*src/components/BookingForm.vue*

```
<script setup lang="ts">
import { ref } from 'vue'

const name = ref('')
const email = ref('')
const tickets = ref<number | ''>(1)
const sent = ref(false)

async function submit() {
  if (tickets.value === '' || tickets.value < 1) return
  const res = await fetch('/api/bookings', {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({ name: name.value, email: email.value, tickets: tickets.value }),
  })
  sent.value = res.ok
}
</script>

<template>
  <form @submit.prevent="submit">
    <label for="name">Name</label>
    <input id="name" v-model.trim="name" required>
    <label for="email">Email</label>
    <input id="email" v-model.trim="email" type="email" required>
    <label for="tickets">Tickets</label>
    <input id="tickets" v-model="tickets" type="number" min="1" max="6" required>
    <button type="submit">Book</button>
    <p v-if="sent" role="status">Booked. Check your inbox.</p>
  </form>
</template>
```

Because the browser validates `required`, `type="email"` and `min` before the submit event fires, `submit()` only runs with a filled-in form. The check on `tickets` stays anyway: its type says it can be `''`, and TypeScript refuses `tickets.value < 1` until that case is ruled out.

`sent` turns on only when your endpoint answers with a success status: `fetch` does not throw on a 404 or a 500.

**Check it stuck**3 questions from the Forms and inputs quiz.

[**Practice now**](https://vuejs.school/learn/beginner/forms-and-inputs?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Tutorial · 6 min read**Computed properties: cache, don't recalculate**](https://vuejs.school/community/blog/computed-properties-cache-dont-recalculate) [Tutorial · 6 min read**v-if, v-show and v-for: the directives you use every day**](https://vuejs.school/community/blog/v-if-v-show-and-v-for)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-06-09","description":"How v-model behaves on text fields, checkboxes, radios and selects, what .lazy, .number and .trim do, and why your numbers sometimes arrive as strings.","name":"Forms and inputs: v-model modifiers, checkboxes and selects","url":"https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects","breadcrumb":{"@id":"https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-06-09","description":"How v-model behaves on text fields, checkboxes, radios and selects, what .lazy, .number and .trim do, and why your numbers sometimes arrive as strings.","headline":"Forms and inputs: v-model modifiers, checkboxes and selects","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~Rm9ybXMgYW5kIGlucHV0czogdi1tb2RlbCBtb2RpZmllcnMsIGNoZWNrYm94ZXMgYW5kIHNlbGVjdHMgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~Rm9ybXMgYW5kIGlucHV0czogdi1tb2RlbCBtb2RpZmllcnMsIGNoZWNrYm94ZXMgYW5kIHNlbGVjdHM%2Cdescription_~SG93IHYtbW9kZWwgYmVoYXZlcyBvbiB0ZXh0IGZpZWxkcywgY2hlY2tib3hlcywgcmFkaW9zIGFuZCBzZWxlY3RzLCB3aGF0IC5sYXp5LCAubnVtYmVyIGFuZCAudHJpbSBkbywgYW5kIHdoeSB5b3VyIG51bWJlcnMgc29tZXRpbWVzIGFycml2ZSBhcyBzdHJpbmdzLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9mb3Jtcy1hbmQtaW5wdXRzLXYtbW9kZWwtbW9kaWZpZXJzLWNoZWNrYm94ZXMtYW5kLXNlbGVjdHMi%2Cs_b7kmPSi4ablkBpkY.png","@type":["Article","BlogPosting"],"articleSection":["Tutorial"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/forms-and-inputs-v-model-modifiers-checkboxes-and-selects#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"Forms and inputs: v-model modifiers, checkboxes and selects","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~Rm9ybXMgYW5kIGlucHV0czogdi1tb2RlbCBtb2RpZmllcnMsIGNoZWNrYm94ZXMgYW5kIHNlbGVjdHMgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~Rm9ybXMgYW5kIGlucHV0czogdi1tb2RlbCBtb2RpZmllcnMsIGNoZWNrYm94ZXMgYW5kIHNlbGVjdHM%2Cdescription_~SG93IHYtbW9kZWwgYmVoYXZlcyBvbiB0ZXh0IGZpZWxkcywgY2hlY2tib3hlcywgcmFkaW9zIGFuZCBzZWxlY3RzLCB3aGF0IC5sYXp5LCAubnVtYmVyIGFuZCAudHJpbSBkbywgYW5kIHdoeSB5b3VyIG51bWJlcnMgc29tZXRpbWVzIGFycml2ZSBhcyBzdHJpbmdzLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9mb3Jtcy1hbmQtaW5wdXRzLXYtbW9kZWwtbW9kaWZpZXJzLWNoZWNrYm94ZXMtYW5kLXNlbGVjdHMi%2Cs_b7kmPSi4ablkBpkY.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~Rm9ybXMgYW5kIGlucHV0czogdi1tb2RlbCBtb2RpZmllcnMsIGNoZWNrYm94ZXMgYW5kIHNlbGVjdHMgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_~Rm9ybXMgYW5kIGlucHV0czogdi1tb2RlbCBtb2RpZmllcnMsIGNoZWNrYm94ZXMgYW5kIHNlbGVjdHM%2Cdescription_~SG93IHYtbW9kZWwgYmVoYXZlcyBvbiB0ZXh0IGZpZWxkcywgY2hlY2tib3hlcywgcmFkaW9zIGFuZCBzZWxlY3RzLCB3aGF0IC5sYXp5LCAubnVtYmVyIGFuZCAudHJpbSBkbywgYW5kIHdoeSB5b3VyIG51bWJlcnMgc29tZXRpbWVzIGFycml2ZSBhcyBzdHJpbmdzLg%2Ceyebrow_~VHV0b3JpYWwgwrcgNSBtaW4gcmVhZA%2Ctint_mint%2Cp_Ii9jb21tdW5pdHkvYmxvZy9mb3Jtcy1hbmQtaW5wdXRzLXYtbW9kZWwtbW9kaWZpZXJzLWNoZWNrYm94ZXMtYW5kLXNlbGVjdHMi%2Cs_b7kmPSi4ablkBpkY.png"}]}
```