SupportWriter space ↗
← Back to the journal
Vue.js

Vue template directives: v-bind, v-on, v-if, v-for, and v-model

Master Vue template syntax to display data, handle events, render conditions and lists, and create two-way bindings for form inputs.

DAdministrator · 01 Oct 2026 · 4 min read

Templates are enhanced HTML

A Vue template is valid HTML extended with special syntax. The most basic form is text interpolation with double curly braces:

<p>Author: {{ author.name }}</p>
<p>Read {{ readCount.toLocaleString('en-US') }} times</p>

Inside an interpolation you can write a single JavaScript expression, but not statements like if or for. Interpolations are always escaped, so they are safe from malicious HTML. Avoid v-html for user-generated content because it can open the door to XSS.

Special attributes prefixed with v- are called directives. Here are the ones you will use most often.

v-bind: binding attributes

v-bind connects an HTML attribute to your data. Its shorthand is a colon.

<img :src="article.image" :alt="article.title">
<a :href="`/articles/${article.slug}`">Read more</a>
<button :disabled="isSaving">Save</button>

Since Vue 3.4, when the attribute name matches the variable name, you can write just :src as shorthand for :src="src".

Dynamic classes and styles

class and style accept objects and arrays:

<li
  :class="{ active: item.id === activeId, done: item.done }"
  :style="{ opacity: item.done ? 0.5 : 1 }"
>
  {{ item.title }}
</li>

The active class is added only when its condition is true. Static and dynamic classes can be combined on the same element.

v-on: handling events

v-on listens for DOM events. Its shorthand is @.

<button type="button" @click="remove(item.id)">Delete</button>
<input @keyup.enter="submit">
<form @submit.prevent="save">...</form>

Modifiers make code shorter:

Modifier Effect
.prevent calls event.preventDefault()
.stop calls event.stopPropagation()
.once the handler runs only once
.self only if the event originated from the element itself
.enter, .esc keyboard key filters

If you need the native event object along with other arguments, use the special $event variable: @click="select(item, $event)".

v-if, v-else-if, v-else, and v-show

Conditional rendering decides whether an element is displayed.

<p v-if="status === 'loading'">Loading data...</p>
<p v-else-if="status === 'error'">Failed to load. Please try again.</p>
<ArticleList v-else :articles="articles" />

v-else-if and v-else must come directly after the v-if element. To group several elements without adding an extra wrapper to the DOM, put v-if on a <template> tag.

v-show also hides elements, but in a different way:

  • v-if actually creates and destroys elements. Toggling is more expensive, but nothing is rendered when the initial condition is false.
  • v-show always renders the element and only toggles display: none. It suits elements that are toggled often, such as dropdowns.

v-for: rendering lists

v-for repeats an element for each item in an array:

<ul>
  <li v-for="(article, index) in articles" :key="article.id">
    {{ index + 1 }}. {{ article.title }}
  </li>
</ul>

The :key attribute is essential. Vue uses keys to identify each item when the list is reordered, extended, or shortened. Use a unique, stable value such as an id. Avoid using index as the key when the order can change, because state inside items (for example input values) may get mixed up.

v-for also works with objects and number ranges:

<span v-for="(value, key) in profile" :key="key">{{ key }}: {{ value }}</span>
<span v-for="n in 5" :key="n">⭐</span>

Don't combine v-if and v-for on the same element. In Vue 3, v-if is evaluated first, so the v-for variable is not yet available. Filter the data with computed instead:

const publishedArticles = computed(() => articles.value.filter((a) => a.published))

v-model: two-way binding

v-model keeps an input's value in sync with state. Vue automatically picks the right property and event for each input type.

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

const name = ref('')
const age = ref(null)
const agreed = ref(false)
const interests = ref([])
const level = ref('beginner')
</script>

<template>
  <input v-model.trim="name" placeholder="Name">
  <input v-model.number="age" type="number">
  <label><input v-model="agreed" type="checkbox"> I agree</label>

  <label><input v-model="interests" type="checkbox" value="vue"> Vue</label>
  <label><input v-model="interests" type="checkbox" value="laravel"> Laravel</label>

  <select v-model="level">
    <option value="beginner">Beginner</option>
    <option value="intermediate">Intermediate</option>
    <option value="advanced">Advanced</option>
  </select>
</template>

v-model modifiers:

  • .trim removes leading and trailing whitespace.
  • .number casts the input to a number.
  • .lazy syncs the value on the change event instead of on every keystroke.

Case study: a simple to-do list

The directives above are enough to build a small app:

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

const newTask = ref('')
const tasks = ref([])
let nextId = 1

const remaining = computed(() => tasks.value.filter((t) => !t.done).length)

function add() {
  if (!newTask.value) {
    return
  }

  tasks.value.push({ id: nextId++, title: newTask.value, done: false })
  newTask.value = ''
}

function remove(id) {
  tasks.value = tasks.value.filter((t) => t.id !== id)
}
</script>

<template>
  <form @submit.prevent="add">
    <input v-model.trim="newTask" placeholder="New task">
    <button>Add</button>
  </form>

  <p v-if="tasks.length === 0">No tasks yet.</p>
  <ul v-else>
    <li v-for="task in tasks" :key="task.id" :class="{ done: task.done }">
      <input v-model="task.done" type="checkbox">
      {{ task.title }}
      <button type="button" @click="remove(task.id)">Delete</button>
    </li>
  </ul>
  <p>{{ remaining }} tasks remaining.</p>
</template>

Exercise

Add "All", "Active", and "Completed" filters to the to-do list above, using one ref for the active filter and one computed for the displayed list.

← Explore more notes