Forms and v-model
Introduction
Forms collect user input—text, choices, and files. v-model creates two-way binding between form controls and reactive state. This chapter covers native inputs, modifiers, v-model on custom components, and simple validation patterns without a full UI library.
Prerequisites
Text Inputs
Code explanation:
v-modeloninput/textareabindsvalue- Always associate
<label>for accessibility
Modifiers
<input v-model.lazy="title" placeholder="Updates on blur" />
<input v-model.number="age" type="number" />
<input v-model.trim="username" />| Modifier | Behavior |
|---|---|
.lazy | Sync on change instead of input |
.number | Cast to number |
.trim | Trim whitespace |
Combine: v-model.trim.lazy.
Checkbox and Radio
Single checkbox (boolean):
<script setup lang="ts">
const agreed = ref(false);
</script>
<template>
<label>
<input v-model="agreed" type="checkbox" />
I agree to terms
</label>
</template>Multiple checkboxes (array):
<script setup lang="ts">
const selected = ref<string[]>([]);
const options = ["Vue", "React", "Svelte"];
</script>
<template>
<label v-for="opt in options" :key="opt">
<input v-model="selected" type="checkbox" :value="opt" />
{{ opt }}
</label>
<p>Picked: {{ selected.join(", ") }}</p>
</template>Radio group:
<script setup lang="ts">
const plan = ref("free");
</script>
<template>
<label>
<input v-model="plan" type="radio" value="free" /> Free
</label>
<label>
<input v-model="plan" type="radio" value="pro" /> Pro
</label>
</template>Select
Single select:
<script setup lang="ts">
const country = ref("");
</script>
<template>
<select v-model="country">
<option disabled value="">Choose…</option>
<option value="us">United States</option>
<option value="jp">Japan</option>
</select>
</template>Multiple select:
<select v-model="tags" multiple>
<option value="vue">Vue</option>
<option value="ts">TypeScript</option>
</select>Form Submit
.prevent on submit avoids full page reload.
v-model on Components
Default custom v-model uses modelValue prop and update:modelValue event:
<!-- TextInput.vue -->
<script setup lang="ts">
const model = defineModel<string>({ default: "" });
</script>
<template>
<input v-model="model" class="text-input" />
</template>Parent:
<TextInput v-model="username" />Vue 3.4+ defineModel simplifies the old pattern:
Multiple v-model Names
<!-- UserForm.vue -->
<script setup lang="ts">
const firstName = defineModel<string>("firstName", { default: "" });
const lastName = defineModel<string>("lastName", { default: "" });
</script>
<template>
<input v-model="firstName" placeholder="First" />
<input v-model="lastName" placeholder="Last" />
</template>Parent:
<UserForm v-model:first-name="first" v-model:last-name="last" />Use kebab-case in templates for multi-word props.
Simple Validation (No Library)
Track touched state and errors:
For large forms, libraries like VeeValidate or UI kits (Element Plus) help—link their official docs when needed.
Warning
Client-side validation improves UX—it does not replace server validation on FastAPI or Django.
Native HTML5 Validation
<form @submit.prevent="onSubmit">
<input v-model="title" required minlength="3" maxlength="100" />
<input v-model="email" type="email" required />
<button type="submit">Save</button>
</form>Browser shows built-in messages; style with ::backdrop / pseudo selectors or call reportValidity() in script.
FAQ
v-model vs + @input?
v-model is sugar for the pair—use v-model unless you need custom parsing.
Why checkbox array empty?
Ensure each checkbox value attribute is set.
defineModel requirements?
Vue 3.4+; enable in vue version from create-vue.
File inputs?
v-model on <input type="file"> binds File list—handle upload separately.
Controlled vs uncontrolled?
Vue components are controlled—state lives in script, not only in DOM.
Form libraries?
Optional for this track—native + computed errors teach the pattern first.