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.