Table of Contents
- Understanding Vue.js Templates
- Single-File Components (SFCs): The Vue Way
- Advanced SFC Features
- Conclusion
- References
Understanding Vue.js Templates
What Are Vue Templates?
At its core, a Vue template is a declarative HTML-based syntax that defines the structure of a component’s UI. Unlike React, which uses JSX (HTML-in-JavaScript), Vue embraces HTML as the template language, making it familiar to developers with HTML/CSS/JS backgrounds.
Vue templates are compiled into optimized JavaScript render functions at build time (or runtime, for development), ensuring high performance. They are not just static HTML—they include special syntax to bind data, handle events, conditionally render elements, loop through lists, and more.
Template Syntax Basics
Vue templates extend HTML with special features to enable dynamic behavior. Let’s break down the key syntax elements:
1. Text Interpolation with Mustache Syntax ({{ }})
The most basic way to inject dynamic data into a template is using double curly braces, or “mustache” syntax:
<template>
<h1>Hello, {{ name }}!</h1>
</template>
<script>
export default {
data() {
return {
name: "Vue Developer" // This data is reactive!
};
}
};
</script>
Here, {{ name }} will be replaced with the value of the name data property. If name updates (e.g., via user input), the template will automatically re-render to reflect the new value—thanks to Vue’s reactivity system.
2. Directives: Adding Logic to Templates
Directives are special attributes prefixed with v- that apply reactive behavior to DOM elements. They are Vue’s way of adding logic (like conditionals, loops, or event handling) directly in the template.
Let’s explore the most common directives:
v-bind: Binding Attributes
Use v-bind to dynamically bind HTML attributes to component data. Shorthand: : (colon).
<!-- Bind the "src" attribute of an image -->
<img v-bind:src="productImage" alt="Product">
<!-- Shorthand syntax -->
<img :src="productImage" alt="Product">
<!-- Bind a class conditionally -->
<div :class="{ active: isActive }">This div is active: {{ isActive }}</div>
In the script, productImage and isActive are reactive data properties.
v-on: Handling Events
Use v-on to listen to DOM events (e.g., clicks, input) and trigger methods. Shorthand: @ (at symbol).
<!-- Call a method on button click -->
<button v-on:click="incrementCounter">Click me!</button>
<!-- Shorthand syntax -->
<button @click="incrementCounter">Click me!</button>
<!-- Pass arguments to methods -->
<button @click="addToCart(product.id)">Add to Cart</button>
In the script:
export default {
data() {
return { counter: 0 };
},
methods: {
incrementCounter() {
this.counter++; // "this" refers to the component instance
}
}
};
v-if/v-else/v-else-if: Conditional Rendering
Conditionally render elements based on a boolean expression. Unlike v-show (which toggles display: none), v-if completely adds/removes elements from the DOM.
<template>
<div v-if="user.isAdmin">Welcome, Admin!</div>
<div v-else-if="user.isLoggedIn">Welcome, {{ user.name }}!</div>
<div v-else>Please log in.</div>
</template>
v-for: Rendering Lists
Loop through an array or object to render multiple elements. Always use the :key attribute with v-for to help Vue track element identity (critical for performance).
<template>
<ul>
<li v-for="(item, index) in todoList" :key="index">
{{ index + 1 }}. {{ item }}
</li>
</ul>
</template>
<script>
export default {
data() {
return {
todoList: ["Learn Vue templates", "Master SFCs", "Build an app"]
};
}
};
</script>
For objects:
<div v-for="(value, key) in user" :key="key">
{{ key }}: {{ value }}
</div>
v-model: Two-Way Data Binding
v-model creates two-way binding between form inputs (e.g., text fields, checkboxes) and component data. It syncs the input’s value with a data property and vice versa.
<template>
<input v-model="username" placeholder="Enter your name">
<p>Hello, {{ username }}!</p> <!-- Updates as the user types -->
</template>
<script>
export default {
data() {
return { username: "" };
}
};
</script>
v-model works with most form elements: <input>, <textarea>, <select>, checkboxes, and radio buttons.
3. Expressions in Templates
Vue templates support JavaScript expressions (not statements) inside mustaches or directives. This allows basic logic like arithmetic, ternary operators, or method calls:
<!-- Arithmetic -->
<p>Total: ${{ price * quantity }}</p>
<!-- Ternary operator -->
<p>Status: {{ isAvailable ? "In Stock" : "Out of Stock" }}</p>
<!-- Method call (returns a value) -->
<p>Formatted Date: {{ formatDate(currentDate) }}</p>
Note: Avoid complex logic in templates—keep expressions simple. Move heavy logic to component methods or computed properties.
Reactivity in Templates
A key strength of Vue is its reactivity system. When you define data properties in the data() function (or using ref/reactive in Composition API), Vue makes them reactive. This means:
- When a reactive property changes, the template automatically updates to reflect the new value.
- Vue tracks dependencies between data and the parts of the template that use them, ensuring efficient updates.
For example:
<template>
<p>Count: {{ count }}</p>
<button @click="count++">Increment</button>
</template>
<script>
export default {
data() {
return { count: 0 }; // "count" is reactive
}
};
</script>
When the button is clicked, count increments, and the template re-renders with the new value—no manual DOM manipulation needed!
Template Best Practices
- Keep templates declarative: Focus on what to render, not how to render it. Let Vue handle the DOM updates.
- Avoid complex logic: Use computed properties or methods for calculations instead of embedding logic in templates.
- Use
v-showfor frequent toggling: If an element is shown/hidden often,v-show(togglesdisplay) is more performant thanv-if(adds/removes from DOM). - Always use
:keywithv-for: This helps Vue optimize re-renders by tracking element identity.
Single-File Components (SFCs): The Vue Way
What Are Single-File Components?
Single-File Components (SFCs) are Vue’s solution to encapsulating a component’s template, logic, and styles in a single file with the .vue extension. Instead of splitting HTML, CSS, and JavaScript into separate files, SFCs group them by component, making it easier to reason about and maintain code.
Example SFC structure:
<!-- ProductCard.vue -->
<template>
<!-- Template (HTML) -->
</template>
<script>
// Logic (JavaScript)
</script>
<style>
// Styles (CSS)
</style>
SFCs are the standard way to build Vue applications, supported by tools like Vite (Vue’s official build tool) and Vue CLI.
Anatomy of an SFC (.vue File)
An SFC has three main sections: <template>, <script>, and <style>. Let’s break them down.
1. <template> Section
The <template> section contains the component’s HTML structure, using the Vue template syntax we covered earlier. There can be only one <template> per SFC, and it must have a single root element (or use a fragment with <template> tag in Vue 3).
Example:
<template>
<div class="product-card">
<img :src="imageUrl" :alt="product.name">
<h3>{{ product.name }}</h3>
<p class="price">${{ product.price }}</p>
<button @click="addToCart">Add to Cart</button>
</div>
</template>
2. <script> Section
The <script> section contains the component’s logic: data, methods, props, lifecycle hooks, and more. There are two primary APIs for writing component logic in Vue:
- Options API: Traditional approach, using options like
data,methods,computed, etc. - Composition API: Modern approach (Vue 3+), using
setup()or the<script setup>syntactic sugar for better code organization and reusability.
Options API Example
<script>
export default {
// Props: data passed from parent component
props: {
product: {
type: Object,
required: true
}
},
// Reactive data
data() {
return {
imageUrl: this.product.image || "default.jpg"
};
},
// Methods
methods: {
addToCart() {
this.$emit("add-to-cart", this.product.id); // Emit event to parent
}
},
// Lifecycle hook: runs when component is mounted
mounted() {
console.log("ProductCard mounted:", this.product.name);
}
};
</script>
Composition API with <script setup> (Vue 3+)
Vue 3 introduced the Composition API, which encourages reusing logic across components. The <script setup> syntax is syntactic sugar for the Composition API, making it more concise:
<script setup>
import { ref, onMounted } from "vue";
// Props (using defineProps)
const props = defineProps({
product: {
type: Object,
required: true
}
});
// Reactive data (using ref)
const imageUrl = ref(props.product.image || "default.jpg");
// Methods
const addToCart = () => {
// Emit event (using defineEmits)
const emit = defineEmits(["add-to-cart"]);
emit("add-to-cart", props.product.id);
};
// Lifecycle hook
onMounted(() => {
console.log("ProductCard mounted:", props.product.name);
});
</script>
<script setup> is now the recommended syntax for Vue 3 SFCs due to its brevity and better TypeScript support.
3. <style> Section
The <style> section contains CSS styles for the component. By default, styles are global, but you can scope them to the component using the scoped attribute—preventing style leakage to other components.
Scoped Styles
Add scoped to the <style> tag to ensure styles only apply to the current component:
<style scoped>
.product-card {
border: 1px solid #e0e0e0;
padding: 1rem;
border-radius: 4px;
}
.price {
color: #e53935; /* Only affects .price in this component */
}
</style>
Vue achieves scoping by adding unique data attributes to component elements (e.g., data-v-abc123) and prefixing CSS selectors with these attributes.
CSS Modules
For more control, use CSS Modules by adding module to the <style> tag. This scopes styles locally and exposes them as a JavaScript object:
<style module>
/* Styles are scoped and accessible via $style */
.card {
background: white;
}
</style>
In the template:
<div :class="$style.card">...</div>
How SFCs Work Under the Hood
SFCs are not directly executable by browsers—they need to be compiled into standard JavaScript and CSS. This is handled by build tools like:
- Vite: Vue’s official build tool, which uses esbuild for fast development and Rollup for production.
- Vue Loader: A Webpack loader for processing SFCs (used in Vue CLI).
The build tool splits the SFC into three parts:
- The
<template>is compiled into a render function. - The
<script>is executed as a module, exporting the component options. - The
<style>is injected into the DOM (or extracted to a CSS file in production).
Benefits of Using SFCs
- Encapsulation: Template, logic, and styles are colocated per component, avoiding global scope pollution.
- Maintainability: All code for a component lives in one file, making it easier to debug and update.
- Tooling Support: IDEs (e.g., VS Code with Volar extension) provide syntax highlighting, autocompletion, and linting for
.vuefiles. - Scoped Styles: Prevent style conflicts across components.
- Reusability: Components can be imported and used anywhere in the app.
Creating Your First SFC: A Practical Example
Let’s build a TodoItem.vue SFC to demonstrate:
<!-- TodoItem.vue -->
<template>
<div class="todo-item">
<input
type="checkbox"
:checked="todo.completed"
@change="toggleCompletion"
>
<span :class="{ completed: todo.completed }">{{ todo.text }}</span>
<button @click="removeTodo">×</button>
</div>
</template>
<script setup>
import { defineProps, defineEmits } from "vue";
// Props: receive todo data from parent
const props = defineProps({
todo: {
type: Object,
required: true,
properties: {
id: { type: Number },
text: { type: String },
completed: { type: Boolean }
}
}
});
// Emits: send events to parent
const emit = defineEmits(["toggle", "remove"]);
// Methods
const toggleCompletion = () => {
emit("toggle", props.todo.id);
};
const removeTodo = () => {
emit("remove", props.todo.id);
};
</script>
<style scoped>
.todo-item {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem;
border-bottom: 1px solid #eee;
}
.completed {
text-decoration: line-through;
color: #888;
}
button {
background: #ff4444;
color: white;
border: none;
border-radius: 50%;
cursor: pointer;
}
</style>
This SFC encapsulates a todo item’s template (checkbox, text, delete button), logic (toggling completion, removing), and scoped styles. It can be imported and used in a parent component like any other module.
Advanced SFC Features
Scoped Styles and CSS Modules
We touched on scoped styles and CSS modules earlier, but they’re worth emphasizing:
- Use
scopedfor simple component styling to avoid conflicts. - Use
modulewhen you need to dynamically apply styles (e.g., conditional classes) or access styles via JavaScript.
Using Composition API with <script setup>
Vue 3’s Composition API is designed for reusing logic across components. The <script setup> syntax simplifies using Composition API by:
- Automatically exporting top-level bindings (no need for
export default). - Providing compile-time sugar for
defineProps,defineEmits, anddefineExpose.
Example with a reusable useLocalStorage composable:
<!-- UserSettings.vue -->
<script setup>
import { useLocalStorage } from "./composables/useLocalStorage";
// Reuse logic to sync "theme" with localStorage
const [theme, setTheme] = useLocalStorage("theme", "light");
</script>
Preprocessors (Sass, Less, Stylus)
SFCs support CSS preprocessors like Sass, Less, or Stylus by adding the lang attribute to the <style> tag:
<!-- Use Sass -->
<style lang="scss" scoped>
.todo-item {
&:hover {
background: #f5f5f5; // Nesting support
}
}
</style>
Vite and Vue CLI have built-in support for preprocessors—just install the required dependency (e.g., sass for SCSS) and start using lang="scss".
Conclusion
Vue.js templates and Single-File Components are foundational to building modern, maintainable web applications with Vue. Templates provide an intuitive, HTML-based way to define your UI, while SFCs encapsulate template, logic, and styles into a single, reusable unit.
By mastering templates—with their directives, reactivity, and declarative syntax—you’ll be able to build dynamic UIs with ease. And with SFCs, you’ll streamline component development, ensuring your code is organized, scoped, and easy to debug.
Whether you’re building a small app or a large-scale project, Vue’s template system and SFCs will help you write clean, efficient, and scalable code.