Vue, JavaScript, Tutorials Vue 3 Template Syntax & Directives: Making HTML Do More
6 min read
3 viewsVue 3 Template Syntax & Directives: Making HTML Do More
Part 5 of the Vue 3 Fundamentals series — mastering v-if, v-for, v-bind, v-on, and v-model.
Vanilla HTML is static. What you write is what you get. Vue’s template syntax changes that.
Vue extends HTML with a set of directives — special attributes prefixed with v- — that let you bind data, respond to events, conditionally render elements, and loop over lists, all directly in your template. No manual DOM updates. No querySelector calls. Just declarative logic that Vue handles for you.
In this article, we’ll cover the most common and maybe most important directives you’ll use every day.
Text Interpolation: {{ }}
We’ve used this already, but let’s be explicit. Double curly braces render a JavaScript expression as text:
<p>{{ message }}</p>
<p>{{ user.name }}</p>
<p>{{ 2 + 2 }}</p>
<p>{{ isLoggedIn ? 'Welcome back' : 'Please log in' }}</p>
This is called text interpolation. Vue evaluates the expression and inserts its value as plain text. Note that it’s text only — you can’t insert raw HTML this way (and for security reasons, you usually don’t want to … DO NOT DO IT !!!!).
v-bind: Binding Attributes Dynamically
v-bind connects a reactive value to an HTML attribute:
<img v-bind:src="imageUrl" v-bind:alt="imageDescription" />
<a v-bind:href="profileLink">View Profile</a>
This is used so often that Vue has a shorthand — a colon ::
<img :src="imageUrl" :alt="imageDescription" />
<a :href="profileLink">View Profile</a>
You’ll see the shorthand everywhere. It’s the standard way to write it.
You can also bind class names and styles dynamically:
<div :class="{ active: isActive, disabled: isDisabled }">...</div>
<div :class="[primaryClass, secondaryClass]">...</div>
<div :style="{ color: textColor, fontSize: fontSize + 'px' }">...</div>
The object syntax for :class is particularly handy — each key is a class name, each value is a boolean that controls whether the class is applied.
v-on: Handling Events
v-on listens for DOM events and runs a function when they fire:
<button v-on:click="handleClick">Click me</button>
<input v-on:input="handleInput" />
<form v-on:submit="handleSubmit">...</form>
Shorthand — the @ symbol:
<button @click="handleClick">Click me</button>
<input @input="handleInput" />
You can also write inline expressions directly in the handler:
<button @click="count++">Increment</button>
<button @click="message = 'Updated!'">Update</button>
Vue also provides event modifiers that let you handle common patterns without writing boilerplate:
<!-- Prevent the default form submission behavior -->
<form @submit.prevent="handleSubmit">...</form>
<!-- Stop the event from bubbling up to parent elements -->
<div @click.stop="handleClick">...</div>
<!-- Only fire when the exact element is clicked, not a child -->
<div @click.self="handleClick">...</div>
These modifiers chain onto the event name with a dot. They’re small but they eliminate a surprising amount of repetitive code.
v-model: Two-Way Binding
v-model creates a two-way connection between a form input and a reactive variable. When the user types, the variable updates. When the variable changes, the input reflects it.
<script setup>
import { ref } from 'vue'
const username = ref('')
const selectedRole = ref('viewer')
const agreeToTerms = ref(false)
</script>
<template>
<input v-model="username" placeholder="Username" />
<p>Hello, {{ username || 'stranger' }}!</p>
<select v-model="selectedRole">
<option value="viewer">Viewer</option>
<option value="editor">Editor</option>
<option value="admin">Admin</option>
</select>
<input type="checkbox" v-model="agreeToTerms" />
<span>{{ agreeToTerms ? 'Agreed' : 'Please agree to continue' }}</span>
</template>
v-model works on text inputs, textareas, checkboxes, radio buttons, and selects. For text-like inputs, it’s often equivalent to :value + @input under the hood, but other controls use different props and events (for example, checkboxes and radios use checked + change, and component v-model uses modelValue + update:modelValue). In practice, the shorthand is clean enough that you’ll almost always use v-model directly.
v-if / v-else-if / v-else: Conditional Rendering
Show or hide elements based on a condition:
<div v-if="isLoggedIn">
<h2>Welcome back!</h2>
</div>
<div v-else-if="isLoading">
<p>Loading...</p>
</div>
<div v-else>
<p>Please log in to continue.</p>
</div>
When v-if evaluates to false, Vue removes the element from the DOM entirely (not just hides it). If you need the element to stay in the DOM but be invisible, use v-show instead:
<!-- v-if: removes from DOM when false -->
<p v-if="showMessage">This may not exist in the DOM</p>
<!-- v-show: always in DOM, toggled with CSS display -->
<p v-show="showMessage">This is always in the DOM</p>
General rule: use v-if when you conditionally render something infrequently. Use v-show when you’re toggling visibility frequently (like a dropdown or modal), since it avoids the cost of creating/destroying DOM elements repeatedly.
v-for: List Rendering
Render a list of items from an array:
<script setup>
import { ref } from 'vue'
const skills = ref(['Vue 3', 'TypeScript', 'Nuxt', 'Pinia'])
</script>
<template>
<ul>
<li v-for="skill in skills" :key="skill">
{{ skill }}
</li>
</ul>
</template>
The :key attribute is strongly recommended and important when using v-for. It gives Vue a way to identify each item uniquely when the list updates, making DOM updates efficient, and it is required in some cases such as component lists or when preserving state. Use a unique identifier — ideally an id from your data. If you have no better option, you can use the index, but it’s not ideal for dynamic lists.
You can also access the index:
<li v-for="(skill, index) in skills" :key="skill">
{{ index + 1 }}. {{ skill }}
</li>
And loop over objects:
<div v-for="(value, key) in userObject" :key="key">
{{ key }}: {{ value }}
</div>
Putting It Together
Here’s a small component that combines everything from this article:
<script setup>
import { ref, computed } from 'vue'
const newSkill = ref('')
const skills = ref(['Vue 3', 'JavaScript'])
const filter = ref('all')
const filteredSkills = computed(() => {
if (filter.value === 'vue') return skills.value.filter(s => s.includes('Vue'))
return skills.value
})
function addSkill() {
if (newSkill.value.trim()) {
skills.value.push(newSkill.value.trim())
newSkill.value = ''
}
}
</script>
<template>
<div>
<input v-model="newSkill" @keyup.enter="addSkill" placeholder="Add a skill" />
<button @click="addSkill">Add</button>
<select v-model="filter">
<option value="all">All skills</option>
<option value="vue">Vue only</option>
</select>
<ul v-if="filteredSkills.length > 0">
<li v-for="skill in filteredSkills" :key="skill">{{ skill }}</li>
</ul>
<p v-else>No skills match your filter.</p>
</div>
</template>
v-model, @click, @keyup.enter, v-model on a select, v-if, v-else, v-for. All in one focused component.
What’s Next
In Article 6, we bring everything together. We’ll build a complete mini Todo app from scratch, using components, reactivity, props, events, and directives — everything from Articles 1 through 5.
This is where it we hope it all should make sense and click.
This is Part 5 of the Vue 3 Fundamentals series. Sources: Vue.js Template Syntax, Vue.js Conditional Rendering, Vue.js List Rendering.