Interpolation
Print a value into the page with double curly braces, and learn which JavaScript is allowed inside them.
{{ javaScriptExpression }}Interpolation prints a value into the page. You write double curly braces in a template with a JavaScript expression inside, and Vue replaces the braces with the result. When the value changes, Vue updates that one text node. You never touch the DOM yourself.
Only expressions
What goes inside the braces must be something that could follow return. So {{ count + 1 }}, {{ ok ? 'yes' : 'no' }} and {{ name.trim() }} all work. A statement does not: {{ const x = 1 }} and {{ if (ok) { } }} are rejected by the compiler. Branching in a template is a ternary, or the v-if directive.
An expression is evaluated against the component's own scope plus a small allowlist of globals, including Math and Date. A helper you attached to window is not visible there.
Where it does not work
Interpolation is for text positions only: between tags, not inside an attribute value. For an attribute, use v-bind. To render markup instead of text there is the v-html directive, which inserts whatever string you give it, so never point it at text a user typed.
One more thing to watch. A function called inside an expression re-runs every time the component updates, so keep it cheap and free of side effects.
Example
<!-- Receipt.vue -->
<script setup lang="ts">
const firstName = 'Ada'
const lastName = 'Lovelace'
const unitPrice = 12.5
const quantity = 3
function formatPrice(amount: number): string {
return `$${amount.toFixed(2)}`
}
</script>
<template>
<section class="receipt">
<h2>{{ firstName }} {{ lastName }}</h2>
<p>{{ quantity }} x {{ formatPrice(unitPrice) }}</p>
<p>Total: {{ formatPrice(unitPrice * quantity) }}</p>
<p>{{ quantity > 1 ? 'Multiple items' : 'One item' }}</p>
</section>
</template>
<style scoped>
.receipt {
font-family: system-ui, sans-serif;
}
.receipt h2 {
margin-bottom: 0.5rem;
}
</style>When to use it
- Show a signed-in person's name and plan in a page header.
- Render a total you derive from a price and a quantity already in scope.
- Display a formatted date or currency string returned by a helper function.
- Show one of two short labels based on a condition, without adding an element.
More examples
v-text is the longhand for a whole text node
v-text sets the element's text content, which is what a mustache does for a text-only element. The difference is that v-text owns the entire element: any content you wrote between the tags is discarded. Mustaches can sit next to other text and other mustaches, so they are what you reach for; recognise v-text when you read someone else's template, and when you want the text to be absent rather than shown as raw braces before the component mounts.
<!-- Greeting.vue -->
<script setup lang="ts">
const message = 'Hello'
</script>
<template>
<!-- these two render identically -->
<p>{{ message }}</p>
<p v-text="message"></p>
<!-- v-text replaces the whole content, so this loses the label -->
<p v-text="message">Label that never appears</p>
</template>v-html, and when it is a security hole
Reach for v-html only when the string is markup you produced or sanitised yourself, such as rendered Markdown from your own content pipeline. Anything a visitor can influence goes through a mustache, which escapes it. Vue does no escaping inside v-html, and Vue bindings inside that inserted markup are ignored rather than compiled, so a script tag or an onerror attribute in that string runs in your page. Sanitise on the server, where an attacker cannot skip the step.
<!-- ArticleBody.vue -->
<script setup lang="ts">
const props = defineProps<{
trustedHtml: string
userComment: string
}>()
</script>
<template>
<!-- from your own CMS, already sanitised on the server -->
<article v-html="props.trustedHtml"></article>
<!-- typed by a visitor: print it as text, never as HTML -->
<p class="comment">{{ props.userComment }}</p>
</template>
<style scoped>
.comment {
white-space: pre-wrap;
}
</style>Keep the expression small by naming the work
A long chain inside braces technically works, but it runs on every update of the component and it cannot be unit tested. Moving it to a plain exported function gives you something you can import into the component, call as {{ initials(name) }}, and test on its own. This is ordinary JavaScript with no Vue in it, which is the point: template expressions should read like a label, not like logic.
// src/lib/format.ts
export function initials(fullName: string): string {
return fullName
.split(' ')
.filter((part) => part.length > 0)
.map((part) => part[0].toUpperCase())
.join('')
}
console.log(initials('Ada Lovelace'))
// Output: AL
console.log(initials('grace brewster hopper'))
// Output: GBH
Discussion