Vue – Template Syntax and Directives

October 18, 20254 min readUpdated 8/24/2026

A Vue template is HTML with two additions: interpolation, written {{ }}, and directives, which are attributes beginning with v-. That is the entire syntax. Everything else in a template is ordinary markup.

Interpolation

<div class="fw-semibold small lh-sm mb-1 text-truncate-2">{{ reel.title }}</div>

What goes inside the braces is a single JavaScript expression, not a statement. So this works:

{{ reel.title.toUpperCase() }}
{{ items.length ? "Some" : "None" }}
{{ formatCount(reel.stats.views) }}

and these do not — there is nowhere for a declaration or a branch to go:

{{ const x = 1 }}
{{ if (ok) { ... } }}

Interpolated text is escaped. A value containing <script> renders as those literal characters, never as an element.

Binding attributes: v-bind and :

{{ }} only works in text content. To make an attribute dynamic you need v-bind, which is almost always written with its : shorthand:

      <img
        :src="reel.video.posterUrl"
        :alt="reel.title"
        loading="lazy"
        class="w-100 h-100"
        style="object-fit: cover"
      />

loading="lazy" is a static attribute; :src and :alt are bound to expressions. Both forms sit side by side in the same tag, which is most of why Vue templates read like HTML.

Three behaviours worth knowing:

null and undefined remove the attribute rather than rendering the string "null".

A boolean attribute follows truthiness. :disabled="false" removes disabled entirely, which is what you want.

v-bind="obj" with no argument spreads every key of an object as its own attribute.

Binding class and style

Vue special-cases these two, because concatenating class strings by hand is miserable. The demo application uses the template-literal form for Bootstrap variants:

<script setup>
import { computed } from "vue";
import { STATUS_META } from "../../utils/format";

const props = defineProps({ status: { type: String, required: true } });
const meta = computed(() => STATUS_META[props.status] ?? { label: props.status, variant: "secondary", icon: "bi-question" });
</script>

<template>
  <span class="badge d-inline-flex align-items-center gap-1" :class="`text-bg-${meta.variant}`">
    <i class="bi" :class="meta.icon"></i>{{ meta.label }}
  </span>
</template>

:class="`text-bg-${meta.variant}`" produces text-bg-success, text-bg-secondary and so on. Note that the static class and the bound :class are merged, not replaced — the badge keeps badge d-inline-flex and gains the variant.

The object form toggles classes by condition, which is usually what you want:

<aside class="admin-sidebar" :class="{ 'is-open': sidebarOpen }">

and the array form combines several:

<div :class="[baseClass, isActive ? 'is-active' : '', { 'is-error': hasError }]">

:style takes an object, with either camelCase or the quoted CSS name:

<div :style="{ opacity: 0.5, objectFit: 'cover' }">
<div :style="{ 'object-fit': 'cover' }">

Handling events: v-on and @

v-on, nearly always written @:

<button @click="confirmDelete = null">Cancel</button>
<button @click="doDelete">Delete</button>

The value can be an inline expression or the name of a method. When it is a method name, the native event is passed as its argument. When you need both a custom argument and the event, $event is available:

<button @click="remove(tag, $event)">

Lesson 11 covers events properly, including the modifiers that make @submit.prevent and @keydown.enter work.

Conditionals and lists, briefly

They get lesson 8 to themselves, but you will see them in every snippet before then:

<p v-if="message" class="text-secondary small mb-3">{{ message }}</p>

<span v-for="tag in modelValue" :key="tag" class="tag-chip">
  #{{ tag }}
</span>

v-html, and when not to use it

Interpolation escapes. If you genuinely need to render HTML from a value, v-html does it:

<div v-html="post.bodyHtml"></div>

Never point this at anything a user can influence. It is a straight route to cross-site scripting: a comment body containing an onerror attribute will run. Use it only for content you generated or have sanitised on the server, and reach for it rarely — the demo application does not use it once.

The other directives you will meet

v-text sets text content, equivalent to interpolation and almost never worth it.

v-once renders an element a single time and then skips it on every later update — a performance escape hatch, covered in lesson 25.

v-pre tells the compiler to leave a block alone, so {{ this }} renders literally. Useful for documenting Vue in Vue, and otherwise not.

v-cloak is a Vue 2 hangover for hiding un-compiled templates. With a build step there is nothing to hide.

Where expressions are evaluated

A template expression can only see what is in scope: the component's own bindings, plus a small allowlist of globals such as Math and Date. It cannot see window, localStorage or an imported module you did not bind. That is deliberate — it keeps templates checkable at build time.

The practical rule that follows: keep template expressions short. Anything with real logic in it belongs in a computed property, which is the next lesson but one.

Next: Reactivity: ref and reactive — what makes any of this update.