On this page
Template syntax and directives: v-bind, v-on, v-if, v-for, v-model
Template syntax overview
Vue's template syntax is a declarative, HTML-based language. The compiler transforms templates into optimized JavaScript render functions — you write familiar HTML with a thin layer of Vue-specific extensions. Every valid HTML document is also a valid Vue template.
The two fundamental building blocks are text interpolation and directives.
Text interpolation
Use double curly braces to render a reactive expression as text:
<p>{{ greeting }}</p>
<p>{{ user.name.toUpperCase() }}</p>
<p>Total: {{ price * quantity }}</p>The expression is re-evaluated and the DOM is updated automatically whenever the referenced reactive data changes. Interpolation is text-only — it HTML-escapes the result, preventing XSS. To render raw HTML, use v-html (only with trusted content).
v-bind — binding attributes
v-bind binds a JavaScript expression to an HTML attribute. The shorthand is a single colon :
<!-- Long form -->
<img v-bind:src="imageUrl" v-bind:alt="imageAlt" />
<!-- Shorthand (preferred) -->
<img :src="imageUrl" :alt="imageAlt" />
<!-- Bind an object of attributes at once -->
<img v-bind="{ src: imageUrl, alt: imageAlt, width: 200 }" />Dynamic class and style bindings
Class and style receive special treatment that allows objects and arrays:
<!-- Object syntax: key is class name, value is boolean -->
<div :class="{ active: isActive, 'text-danger': hasError }"></div>
<!-- Array syntax: mix static and dynamic classes -->
<div :class="['base-class', isActive ? 'active' : '', extraClass]"></div>
<!-- Style object -->
<div :style="{ color: brandColor, fontSize: `${size}px` }"></div>
<!-- Array of style objects (merged) -->
<div :style="[baseStyles, overrideStyles]"></div>v-on — event handling
v-on attaches event listeners. The shorthand is @:
<!-- Inline handler -->
<button type="button" @click="count++">+</button>
<!-- Method reference (receives the Event object) -->
<button type="button" @click="handleClick">Click</button>
<!-- Method call with argument -->
<button type="button" @click="remove(item.id)">Delete</button>
<!-- Inline with $event for the native event -->
<input @change="handleChange($event.target.value)" />Event modifiers
Vue's event modifiers prevent repetitive boilerplate:
<!-- Equivalent to event.preventDefault() -->
<form @submit.prevent="onSubmit">...</form>
<!-- Stop event propagation -->
<a @click.stop="doThis">...</a>
<!-- Chain modifiers -->
<a @click.stop.prevent="doThat">...</a>
<!-- Only fires once -->
<button type="button" @click.once="doOnce">...</button>
<!-- Key modifiers -->
<input @keyup.enter="submit" />
<input @keyup.esc="clear" />v-if, v-else-if, v-else — conditional rendering
v-if conditionally renders an element. The element is completely removed from the DOM when the condition is falsy — event listeners and child components are destroyed and recreated.
<div v-if="status === 'loading'">Loading...</div>
<div v-else-if="status === 'error'">Error: {{ errorMessage }}</div>
<div v-else>Content loaded!</div>Use <template> as an invisible wrapper when you need to conditionally group multiple elements without adding an extra DOM node:
<template v-if="isLoggedIn">
<nav>...</nav>
<aside>...</aside>
</template>v-show — toggle visibility
v-show keeps the element in the DOM and toggles display: none. It has higher initial render cost but cheaper toggle cost than v-if:
<div v-show="isVisible">This is always in the DOM</div>v-for — list rendering
v-for renders a list of items. Always provide a :key with a unique, stable value:
<!-- Array -->
<li v-for="item in items" :key="item.id">{{ item.name }}</li>
<!-- With index -->
<li v-for="(item, index) in items" :key="item.id">
{{ index + 1 }}. {{ item.name }}
</li>
<!-- Object properties -->
<li v-for="(value, key, index) in myObject" :key="key">
{{ index }}: {{ key }} = {{ value }}
</li>
<!-- Range -->
<span v-for="n in 5" :key="n">{{ n }}</span>v-for + v-if together
When both are needed on the same element, wrap the list in a <template v-for> and apply v-if inside:
<!-- CORRECT: v-for on the wrapper, v-if on the item -->
<template v-for="user in users" :key="user.id">
<li v-if="user.isActive">{{ user.name }}</li>
</template>Never put v-if and v-for on the same element — v-if has higher priority in Vue 3 and the loop variable would not be in scope.
v-model — two-way binding
v-model creates a two-way binding between a form element and a reactive variable. It is syntactic sugar over :value + @input:
<!-- These two are equivalent -->
<input v-model="name" />
<input :value="name" @input="name = $event.target.value" />v-model adapts to different input types:
| Element | Bound property | Event |
|---|---|---|
<input type="text"> |
value |
input |
<input type="checkbox"> |
checked |
change |
<input type="radio"> |
checked |
change |
<select> |
value |
change |
<textarea> |
value |
input |
v-model modifiers
<!-- Trim whitespace automatically -->
<input v-model.trim="name" />
<!-- Cast to number -->
<input type="number" v-model.number="age" />
<!-- Update on change event instead of input (less frequent) -->
<input v-model.lazy="search" />v-once and v-memo
For performance-critical paths, v-once renders the element once and never re-renders it:
<h1 v-once>{{ staticTitle }}</h1>v-memo re-renders only when one of its dependency values changes — useful for large lists:
<div v-for="item in list" :key="item.id" v-memo="[item.selected]">
<!-- Only re-renders when item.selected changes -->
<ExpensiveComponent :item="item" />
</div>Practice
- Build a search filter: Create a component with a text input (
v-model) and a list rendered withv-for. Filter the list in acomputed()property using the search term. - Toggle form sections: Create a multi-step form using
v-if/v-else-if/v-elseto show different sections based on acurrentStepref. - Dynamic class binding: Create a set of buttons where clicking one makes it "active". Use
:classwith an object to apply anactiveCSS class to the selected button.
In the next lesson, we will go deep into Vue's reactivity system — understanding ref(), reactive(), and toRefs() at a fundamental level.
<script setup lang="ts">
import { ref } from 'vue'
interface Todo {
id: number
text: string
done: boolean
}
const newText = ref('')
const filter = ref<'all' | 'active' | 'done'>('all')
const todos = ref<Todo[]>([
{ id: 1, text: 'Learn Vue 3.5', done: true },
{ id: 2, text: 'Build a real project', done: false },
{ id: 3, text: 'Explore Pinia', done: false },
])
const filtered = computed(() => {
if (filter.value === 'active') return todos.value.filter(t => !t.done)
if (filter.value === 'done') return todos.value.filter(t => t.done)
return todos.value
})
function addTodo() {
const text = newText.value.trim()
if (!text) return
todos.value.push({ id: Date.now(), text, done: false })
newText.value = ''
}
function removeTodo(id: number) {
todos.value = todos.value.filter(t => t.id !== id)
}
</script>
<template>
<section>
<!-- v-model: two-way binding -->
<form @submit.prevent="addTodo">
<input
v-model="newText"
type="text"
placeholder="New task…"
aria-label="New task"
/>
<button type="submit">Add</button>
</form>
<!-- v-model on a select -->
<select v-model="filter" aria-label="Filter tasks">
<option value="all">All</option>
<option value="active">Active</option>
<option value="done">Done</option>
</select>
<!-- v-if / v-else -->
<p v-if="filtered.length === 0">No tasks to show.</p>
<!-- v-for with :key -->
<ul v-else>
<li v-for="todo in filtered" :key="todo.id">
<!-- v-bind shorthand : -->
<input
type="checkbox"
:id="`todo-${todo.id}`"
v-model="todo.done"
/>
<!-- dynamic class binding -->
<label
:for="`todo-${todo.id}`"
:class="{ done: todo.done }"
>{{ todo.text }}</label>
<!-- v-on shorthand @ -->
<button
type="button"
@click="removeTodo(todo.id)"
aria-label="Remove task"
>✕</button>
</li>
</ul>
</section>
</template>
<style scoped>
label.done { text-decoration: line-through; color: #9ca3af; }
ul { list-style: none; padding: 0; }
li { display: flex; align-items: center; gap: 0.5rem; margin-block: 0.25rem; }
</style>
Sign in to track your progress