Vue.js 5 🟢 Template Syntax Fundamentals and Interpolation ({{ }})
Vue templates are HTML with a small set of additions. The most fundamental of those additions is the mustache binding, written as double curly braces. Everything else in the template syntax — directives, modifiers, shorthand — builds on the same idea: the template declares what the UI should look like as a function of the component’s state, and Vue keeps the DOM in sync with that state. The mustache is the simplest expression of this idea, and it is where every Vue template begins.
The mustache binding renders a JavaScript expression into the DOM. The expression is evaluated against the component’s context, and the result is inserted as text. When the underlying state changes, Vue updates the DOM to reflect the new value. This chapter covers the mustache, its rules, the expressions it accepts, the difference between interpolation and attribute binding, and the patterns that make template expressions readable.
Key point: The mustache binding {{ expression }} renders a JavaScript expression as text. It can appear in element content and in attribute values, but not in HTML attribute names or event handler attributes. The expression has access to the component’s state, computed properties, methods, and props. Use directives (v-bind, v-on) for attributes and events, not the mustache.
Why mustache interpolation exists
The static markup problem. HTML describes a fixed document. A dynamic application needs markup whose content depends on the application’s state: a user’s name, a count, a formatted date. The mustache binding expresses this dependency directly in the template, and the framework handles the update.
The manual DOM problem. Without a framework, updating text requires selecting the element, setting textContent, and repeating that logic wherever the value changes. The mustache binding declares the relationship once, and Vue keeps it in sync.
The separation problem. The template declares the structure and the bindings. The component declares the state and the logic. The separation keeps the markup readable and the logic testable. The mustache is the boundary between them.
The safety problem. The mustache renders the result as text, not HTML. A user’s input that contains <script> is displayed literally, not executed. This prevents the most common XSS vector by default.
The readability problem. A template with mustaches reads like the output. The reader can see where each value appears, and the component’s state can be changed without touching the template.
a. Basic interpolation
The mustache is written as double curly braces around an expression. The expression is evaluated, and the result is rendered as text.
<script setup lang="ts">
const message = 'Hello, Vue!';
const name = 'Alice';
const count = 42;
</script>
<template>
<p>{{ message }}</p>
<p>Hello, {{ name }}!</p>
<p>Count: {{ count }}</p>
</template>
The rendered output has the values substituted:
<p>Hello, Vue!</p>
<p>Hello, Alice!</p>
<p>Count: 42</p>
The expression can be any JavaScript expression, not just a variable reference. Arithmetic, ternaries, method calls, and template literals are all valid.
<template>
<p>{{ count + 1 }}</p>
<p>{{ count > 10 ? 'many' : 'few' }}</p>
<p>{{ name.toUpperCase() }}</p>
<p>{{ `Hello, ${name}!` }}</p>
</template>
The mustache renders the result of the expression. If the result is not a string, Vue converts it. Numbers become their string representation, null and undefined render as empty strings, and objects render as [object Object] unless a formatter is used.
b. What can appear in a mustache
The expression inside the mustache has access to the component’s context: the refs, reactive objects, computed values, methods, and props that are exposed to the template.
<script setup lang="ts">
import { ref, computed } from 'vue';
const firstName = ref('Alice');
const lastName = ref('Nguyen');
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
function greet(): string {
return `Hello, ${fullName.value}!`;
}
</script>
<template>
<p>{{ firstName }}</p>
<p>{{ fullName }}</p>
<p>{{ greet() }}</p>
</template>
In <script setup>, the refs are automatically unwrapped in the template. firstName refers to the value, not the ref object. In the Options API, the template accesses properties on the component instance, and the same unwrapping applies to data and computed.
The expression cannot contain statements. if, for, while, and variable declarations are statements, not expressions, and are not valid inside a mustache.
<!-- Invalid -->
<p>{{ if (count > 0) { 'yes' } }}</p>
<p>{{ let x = 5 }}</p>
<!-- Valid -->
<p>{{ count > 0 ? 'yes' : 'no' }}</p>
The mustache is for expressions. Control flow belongs in directives.
c. Interpolation versus attribute binding
The mustache can be used in element content and in attribute values, but not in attribute names or event handler attributes.
<!-- Valid: mustache in content -->
<p>{{ message }}</p>
<!-- Valid: mustache in an attribute value -->
<div title="{{ message }}">Hover me</div>
<!-- Invalid: mustache in an attribute name -->
<div {{ attrName }}="value">...</div>
<!-- Invalid: mustache in an event handler -->
<button @click="{{ handler }}">Click</button>
For attribute binding, the preferred syntax is v-bind, not the mustache.
<!-- Preferred -->
<div :title="message">Hover me</div>
<!-- Works but discouraged -->
<div title="{{ message }}">Hover me</div>
The v-bind shorthand is a colon before the attribute name. It binds the attribute to an expression, and the expression can be any JavaScript expression.
<img :src="imageUrl" :alt="imageAlt" />
<a :href="link">Link</a>
<button :disabled="isDisabled">Submit</button>
For boolean attributes, v-bind handles the presence and absence of the attribute correctly. A mustache in an attribute value would set the attribute to the string "false", which is still truthy in HTML.
<!-- Correct: attribute is removed when false -->
<button :disabled="isDisabled">Submit</button>
<!-- Wrong: attribute is present with value "false" -->
<button disabled="{{ isDisabled }}">Submit</button>
This is one reason v-bind is preferred over mustache interpolation in attributes.
For event handlers, the mustache is not valid. The v-on directive, abbreviated as @, is used instead.
<button @click="handleClick">Click</button>
<button @click="count++">Increment</button>
<button @click="handleClick($event)">Click with event</button>
d. Expressions in the template
A template expression is evaluated against the component’s context. It can reference:
| Source | Example |
|---|---|
| Refs | {{ count }} |
| Reactive objects | {{ user.name }} |
| Computed values | {{ fullName }} |
| Methods | {{ formatDate(now) }} |
| Props | {{ title }} |
| Constants | {{ MAX_ITEMS }} |
| Template literals | {{ `Hello, ${name}` }} |
| Ternaries | {{ isActive ? 'on' : 'off' }} |
| Method chains | {{ items.filter(i => i.active).length }} |
The expression can be complex, but complexity hurts readability. A long expression in the template is better extracted into a computed property.
<!-- Hard to read -->
<p>{{ items.filter(i => i.active).map(i => i.name).join(', ') }}</p>
<!-- Better -->
<p>{{ activeNames }}</p>
const activeNames = computed(() =>
items.value.filter(i => i.active).map(i => i.name).join(', ')
);
Computed properties are cached and re-evaluated only when their dependencies change. A method call in the template runs on every re-render, which is less efficient and harder to reason about.
e. Rendering HTML with v-html
The mustache renders text, not HTML. To render raw HTML, the v-html directive is used.
<script setup lang="ts">
const rawHtml = '<strong>Bold</strong> and <em>italic</em>';
</script>
<template>
<div v-html="rawHtml"></div>
</template>
The rendered output contains the actual HTML elements. The v-html directive is a security risk when the content comes from user input, because it can execute scripts and inject event handlers. It should be used only with trusted content, and the content should be sanitized before it is passed to the directive.
<!-- Dangerous if userInput is untrusted -->
<div v-html="userInput"></div>
The mustache is safe by default because it renders text. v-html is unsafe by default because it renders HTML. The difference matters, and the safe option should be the default choice.
f. Mustache limitations
The mustache has specific limitations that the template syntax works around.
| Limitation | Reason | Workaround |
|---|---|---|
| Cannot contain statements | Only expressions are valid | Use directives or computed properties |
| Cannot be used in attribute names | Attribute names are static | Use v-bind with a dynamic key |
| Cannot be used in event handlers | Event binding is a directive | Use v-on |
| Renders text, not HTML | Security and predictability | Use v-html for trusted content |
Renders [object Object] | Objects are not rendered as text | Use a formatter or computed |
For a dynamic attribute name, Vue provides v-bind with an argument:
<div v-bind:[attrName]="value">...</div>
The square brackets mark the argument as dynamic. The expression inside is evaluated, and its result is used as the attribute name. This is the only way to bind to a dynamic attribute name; the mustache cannot appear in an attribute name.
For an object that should render as text, a formatter is used:
<p>{{ JSON.stringify(user) }}</p>
Or, more commonly, a computed property that formats the object:
const userLabel = computed(() => `${user.value.firstName} ${user.value.lastName}`);
Complete Example Session
<!-- ============================================
PART 1: BASIC INTERPOLATION
============================================ -->
<script setup lang="ts">
const message = 'Hello, Vue!';
</script>
<template>
<p>{{ message }}</p>
</template>
<!-- ============================================
PART 2: EXPRESSIONS IN MUSTACHE
============================================ -->
<script setup lang="ts">
import { ref } from 'vue';
const count = ref(42);
const name = ref('Alice');
</script>
<template>
<p>{{ count + 1 }}</p>
<p>{{ count > 10 ? 'many' : 'few' }}</p>
<p>{{ name.toUpperCase() }}</p>
<p>{{ `Hello, ${name}!` }}</p>
</template>
<!-- ============================================
PART 3: COMPUTED VALUES IN MUSTACHE
============================================ -->
<script setup lang="ts">
import { ref, computed } from 'vue';
const firstName = ref('Alice');
const lastName = ref('Nguyen');
const fullName = computed(() => `${firstName.value} ${lastName.value}`);
</script>
<template>
<p>{{ fullName }}</p>
</template>
<!-- ============================================
PART 4: METHOD CALLS IN MUSTACHE
============================================ -->
<script setup lang="ts">
function formatPrice(value: number): string {
return `$${value.toFixed(2)}`;
}
</script>
<template>
<p>{{ formatPrice(19.99) }}</p>
</template>
<!-- ============================================
PART 5: MUSTACHE IN CONTENT, V-BIND IN ATTRIBUTES
============================================ -->
<script setup lang="ts">
const message = 'Hello';
const imageUrl = '/logo.png';
</script>
<template>
<p>{{ message }}</p>
<img :src="imageUrl" alt="Logo" />
</template>
<!-- ============================================
PART 6: BOOLEAN ATTRIBUTE WITH V-BIND
============================================ -->
<script setup lang="ts">
import { ref } from 'vue';
const isDisabled = ref(false);
</script>
<template>
<button :disabled="isDisabled">Submit</button>
</template>
<!-- ============================================
PART 7: DYNAMIC ATTRIBUTE NAME
============================================ -->
<script setup lang="ts">
import { ref } from 'vue';
const attrName = ref('title');
const value = ref('Hello');
</script>
<template>
<div v-bind:[attrName]="value">Content</div>
</template>
<!-- ============================================
PART 8: V-HTML FOR TRUSTED CONTENT
============================================ -->
<script setup lang="ts">
const rawHtml = '<strong>Bold</strong>';
</script>
<template>
<div v-html="rawHtml"></div>
</template>
<!-- ============================================
PART 9: EXTRACT COMPLEX EXPRESSION TO COMPUTED
============================================ -->
<script setup lang="ts">
import { ref, computed } from 'vue';
const items = ref([
{ name: 'A', active: true },
{ name: 'B', active: false },
]);
const activeNames = computed(() =>
items.value.filter(i => i.active).map(i => i.name).join(', ')
);
</script>
<template>
<p>{{ activeNames }}</p>
</template>
<!-- ============================================
PART 10: OBJECT FORMATTING
============================================ -->
<script setup lang="ts">
import { ref, computed } from 'vue';
const user = ref({ firstName: 'Alice', lastName: 'Nguyen' });
const userLabel = computed(() => `${user.value.firstName} ${user.value.lastName}`);
</script>
<template>
<p>{{ userLabel }}</p>
</template>
These ten parts cover basic interpolation, expressions in the mustache, computed values, method calls, content versus attributes, boolean attributes, dynamic attribute names, v-html, extracting complex expressions to computed properties, and object formatting.
Quick Reference
Mustache Syntax
| Form | Example |
|---|---|
| Variable | {{ name }} |
| Expression | {{ count + 1 }} |
| Ternary | {{ x ? 'a' : 'b' }} |
| Method call | {{ format(x) }} |
| Template literal | {{ `Hello, ${name}` }} |
| Method chain | {{ items.length }} |
Valid versus Invalid
| Valid | Invalid |
|---|---|
{{ count }} | {{ if (x) { } }} |
{{ x > 0 ? 'yes' : 'no' }} | {{ let y = 1 }} |
{{ user.name }} | {{ {{ nested }} }} |
Interpolation by Position
| Position | Mustache | Directive |
|---|---|---|
| Element content | Yes | v-text, v-html |
| Attribute value | Yes (discouraged) | v-bind |
| Attribute name | No | v-bind:[name] |
| Event handler | No | v-on |
Binding Directives
| Directive | Shorthand | Purpose |
|---|---|---|
v-bind:attr | :attr | Bind an attribute |
v-on:event | @event | Bind an event |
v-bind:[name] | :[name] | Dynamic attribute name |
Rendering Rules
| Value | Rendered |
|---|---|
| String | As-is |
| Number | String representation |
null | Empty string |
undefined | Empty string |
true / false | "true" / "false" |
| Object | [object Object] |
| Array | Joined with commas |
Best Practices
✅ Do This:
<!-- Simple expressions in the mustache -->
<p>{{ count }}</p>
<p>{{ x > 0 ? 'on' : 'off' }}</p>
<!-- Use v-bind for attributes -->
<img :src="url" :alt="alt" />
<!-- Extract complex expressions to computed -->
<p>{{ activeNames }}</p>
<!-- Use v-html only for trusted content -->
<div v-html="sanitizedHtml"></div>
<!-- Use dynamic attribute names with v-bind -->
<div v-bind:[attrName]="value">...</div>
❌ Don’t Do This:
<!-- Statements in the mustache -->
<p>{{ if (x) { } }}</p> <!-- ❌ -->
<!-- Mustache in event handlers -->
<button @click="{{ handler }}"> <!-- ❌ -->
<!-- Complex expressions in the template -->
<p>{{ items.filter(i => i.active).map(i => i.name).join(', ') }}</p> <!-- ❌ -->
<!-- Mustache in boolean attributes -->
<button disabled="{{ isDisabled }}"> <!-- ❌ -->
<!-- v-html with untrusted content -->
<div v-html="userInput"></div> <!-- ❌ -->
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
[object Object] rendered | Object used in the mustache | Use a computed formatter |
{{ }} visible in output | Mustache not compiled | Check the template is inside the <template> |
Attribute set to "false" | Mustache in a boolean attribute | Use :disabled="bool" |
| Expression error | Method or property not exposed | Declare it in <script setup> |
| Statement not allowed | Used if or let in the mustache | Use a ternary or computed |
v-html content not updating | Cached by the browser | Force a key or use a component |
| Undefined rendered as empty | Expected but confusing | Provide a default value |
Real-World Examples
1. Simple Text
<p>{{ message }}</p>
2. Conditional Text
<p>{{ isLoading ? 'Loading...' : 'Done' }}</p>
3. Formatted Number
<p>{{ price.toFixed(2) }}</p>
4. Computed Full Name
<p>{{ fullName }}</p>
5. Attribute Binding
<img :src="user.avatar" :alt="user.name" />
6. Boolean Attribute
<button :disabled="isSubmitting">Submit</button>
7. Dynamic Attribute Name
<div v-bind:[dataAttr]="value">...</div>
8. Trusted HTML
<div v-html="sanitizedContent"></div>
9. List Length
<p>{{ items.length }} items</p>
10. Filtered List in Computed
<p>{{ activeItemsLabel }}</p>
Visual
Mustache Rendering
┌──────────────────────────────────────────────────────────────┐
│ State: const name = 'Alice' │
│ │
│ Template: <p>Hello, {{ name }}!</p> │
│ │
│ Evaluated: <p>Hello, Alice!</p> │
│ │
│ State changes: name = 'Bob' │
│ │ │
│ ▼ │
│ DOM updated: <p>Hello, Bob!</p> │
└──────────────────────────────────────────────────────────────┘
Content vs Attributes
┌──────────────────────────────────────────────────────────────┐
│ CONTENT (mustache): │
│ <p>{{ message }}</p> ✅ │
│ │
│ ATTRIBUTE VALUE (v-bind preferred): │
│ <img :src="url" /> ✅ │
│ <img src="{{ url }}" /> ⚠️ works but discouraged │
│ │
│ ATTRIBUTE NAME: │
│ <div {{ name }}="value"> ❌ │
│ <div v-bind:[name]="value"> ✅ │
│ │
│ EVENT HANDLER: │
│ <button @click="{{ fn }}"> ❌ │
│ <button @click="fn"> ✅ │
└──────────────────────────────────────────────────────────────┘
Expression vs Statement
┌──────────────────────────────────────────────────────────────┐
│ EXPRESSIONS (valid): │
│ {{ count + 1 }} │
│ {{ x > 0 ? 'yes' : 'no' }} │
│ {{ user.name.toUpperCase() }} │
│ │
│ STATEMENTS (invalid): │
│ {{ if (x) { } }} │
│ {{ for (const i of items) { } }} │
│ {{ let y = 5 }} │
│ │
│ Statements belong in the script, not the template. │
└──────────────────────────────────────────────────────────────┘
Complex Expression Extraction
┌──────────────────────────────────────────────────────────────┐
│ BEFORE: │
│ <p>{{ items.filter(i => i.active).map(i => i.name) }} │
│ │
│ AFTER: │
│ const activeNames = computed(() => │
│ items.value.filter(i => i.active).map(i => i.name) │
│ ); │
│ <p>{{ activeNames }}</p> │
│ │
│ Computed is cached and re-evaluates only when needed. │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Mustache | {{ expression }} |
| Position | Content and attribute values |
| Invalid positions | Attribute names, event handlers |
| Expression | JavaScript expression, not statement |
| Renders | Text, not HTML |
| HTML rendering | v-html (trusted content only) |
| Attribute binding | v-bind or : |
| Event binding | v-on or @ |
| Dynamic attribute name | v-bind:[name] |
| Complex logic | Extract to a computed property |
Key takeaways:
- The mustache renders a JavaScript expression as text. It can appear in element content and in attribute values, and the result is inserted into the DOM. When the state changes, Vue updates the DOM.
- The expression is evaluated against the component’s context. Refs, reactive objects, computed values, methods, props, and constants are all accessible in the template.
- The mustache accepts expressions, not statements.
if,for, and variable declarations are statements and are not valid inside the mustache. Use ternaries, computed properties, or directives instead. - Use
v-bindfor attributes, not the mustache. The mustache in an attribute value works for strings but fails for boolean attributes.:disabled="bool"sets the attribute correctly;disabled="{{ bool }}"sets it to the string"false", which is still truthy. - Use
v-onfor event handlers. The mustache is not valid in an event handler attribute. The@click="handler"syntax binds a function reference. - The mustache renders text, not HTML. This is safe by default. To render HTML, use
v-html, which is a security risk with untrusted content. - Extract complex expressions to computed properties. A computed property is cached and readable. A long method chain in the template runs on every render and is hard to read.
Remember: The mustache binding is the simplest and most fundamental part of Vue’s template syntax. It renders an expression as text, and it updates when the state changes. Everything else in the template syntax — directives, modifiers, shorthand — exists to handle the cases the mustache cannot: attributes, events, control flow, and HTML. The mustache is for content, v-bind is for attributes, and v-on is for events. The expression inside the mustache is JavaScript, but only expressions, not statements. When the expression grows complex, it belongs in a computed property. Understanding the mustache is understanding the boundary between the template and the component’s state.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!