Vue.js Single File Components
A Single File Component (SFC) is a special file format with the .vue extension. It puts a component's template, logic, and styles into one clean, organized file. SFCs are the standard way to build Vue applications with a build tool like Vite.
The Problem SFCs Solve
When you write components using the CDN approach, the template is a JavaScript string, the logic is mixed with registration code, and styles live in a separate CSS file. For small examples this is fine, but for real applications with dozens of components, managing everything separately becomes difficult.
SFCs solve this by keeping everything that belongs to one component together in one place.
Diagram: CDN Component vs Single File Component
CDN approach — scattered across files: index.html → template as a string inside JS app.js → component logic (data, methods) styles.css → component styles (hard to isolate) Hard to find what belongs to which component. SFC approach — everything in one file: UserCard.vue ├── <template> → HTML structure ├── <script> → JavaScript logic └── <style> → CSS styles (scoped to this component) Everything for UserCard lives in UserCard.vue.
The Structure of a .vue File
Every .vue file has up to three sections, each in its own block:
<template>
<div class="card">
<h3>{{ title }}</h3>
<p>{{ description }}</p>
</div>
</template>
<script>
export default {
name: "InfoCard",
props: {
title: String,
description: String
}
};
</script>
<style scoped>
.card {
border: 1px solid #ccc;
border-radius: 8px;
padding: 16px;
}
h3 {
margin: 0 0 8px;
}
</style>
What Each Block Does
- <template> — The HTML structure. Only one template block per file. Behaves exactly like a regular Vue template.
- <script> — The component's JavaScript. You export a default object (the Options API) or use
setup()(the Composition API). - <style scoped> — CSS that applies only to this component. The
scopedattribute prevents styles from leaking into other components.
How Vite Processes SFCs
A browser cannot read a .vue file directly. Vite (your build tool) takes every .vue file, splits it into its three sections, compiles each one, and produces regular JavaScript that the browser understands.
Diagram: Vite SFC Compilation
UserCard.vue
┌──────────────────────────────┐
│ <template> ... </template> │
│ <script> ... </script> │──▶ Vite processes
│ <style scoped> ... </style> │
└──────────────────────────────┘
│
▼
Compiled JavaScript bundle
(template → render function)
(script → component object)
(style → injected CSS with unique class prefix)
│
▼
Browser loads and runs the result
User sees the rendered component
Project File Layout with SFCs
A typical Vue project using Vite and SFCs looks like this:
src/
├── main.js ← Creates and mounts the Vue app
├── App.vue ← Root component
└── components/
├── NavBar.vue
├── UserCard.vue
├── ProductList.vue
└── Footer.vue
main.js — The Entry Point
import { createApp } from "vue";
import App from "./App.vue";
createApp(App).mount("#app");
App.vue — The Root Component
<template>
<NavBar />
<main>
<UserCard
v-for="user in users"
:key="user.id"
:name="user.name"
:role="user.role"
/>
</main>
<AppFooter />
</template>
<script>
import NavBar from "./components/NavBar.vue";
import UserCard from "./components/UserCard.vue";
import AppFooter from "./components/Footer.vue";
export default {
components: { NavBar, UserCard, AppFooter },
data() {
return {
users: [
{ id: 1, name: "Alice", role: "Designer" },
{ id: 2, name: "Bob", role: "Developer" }
]
};
}
};
</script>
Importing and Using Components
To use another SFC inside your component, import it and register it in the components option.
<!-- UserCard.vue -->
<template>
<div class="user-card">
<AvatarImage :src="avatarUrl" />
<h3>{{ name }}</h3>
</div>
</template>
<script>
import AvatarImage from "./AvatarImage.vue";
export default {
components: { AvatarImage },
props: {
name: String,
avatarUrl: String
}
};
</script>
Diagram: Component Import Tree
main.js
└── App.vue
├── NavBar.vue
├── UserCard.vue
│ └── AvatarImage.vue
└── Footer.vue
Each file imports only what it directly uses.
Vue assembles the full component tree automatically.
Scoped Styles — Isolation in Action
Adding scoped to a <style> block means those CSS rules apply only to elements within that component. They do not affect any other component on the page.
How Scoping Works Under the Hood
Vite adds a unique data attribute to every element in the component and adds the same attribute to the CSS selectors.
You write:
<style scoped>
h3 { color: blue; }
</style>
Vite compiles it to:
<h3 data-v-7ba5bd90>Card Title</h3>
h3[data-v-7ba5bd90] { color: blue; }
Only h3 elements inside this component get the blue color.
h3 elements in other components are not affected.
Diagram: Scoped vs Non-Scoped Styles
Without scoped:
NavBar.vue adds: h3 { color: red; }
→ ALL h3 on the page turn red (including UserCard's h3)
With scoped:
NavBar.vue adds: h3[data-v-abc] { color: red; }
→ Only NavBar's h3 turns red
→ UserCard's h3 keeps its own color
Global Styles vs Scoped Styles
You can have both a scoped block and a non-scoped block in the same file. The non-scoped block applies globally.
<style>
/* Global — applies everywhere */
body { font-family: Arial, sans-serif; }
</style>
<style scoped>
/* Scoped — applies only to this component */
.card { padding: 16px; }
</style>
The lang Attribute for Preprocessors
SFCs support CSS preprocessors like SCSS and JavaScript supersets like TypeScript. Add the lang attribute to activate them.
<!-- TypeScript -->
<script lang="ts">
export default {
data(): { count: number } {
return { count: 0 };
}
};
</script>
<!-- SCSS -->
<style scoped lang="scss">
.card {
padding: 16px;
h3 {
color: $primary-color;
}
}
</style>
A Complete SFC Example: Countdown Timer
<!-- CountdownTimer.vue -->
<template>
<div class="timer">
<p class="display">{{ formattedTime }}</p>
<button @click="startTimer" :disabled="running">Start</button>
<button @click="resetTimer">Reset</button>
</div>
</template>
<script>
export default {
name: "CountdownTimer",
props: {
startSeconds: {
type: Number,
default: 60
}
},
data() {
return {
remaining: this.startSeconds,
running: false,
intervalID: null
};
},
computed: {
formattedTime() {
const m = Math.floor(this.remaining / 60);
const s = this.remaining % 60;
return `${String(m).padStart(2, "0")}:${String(s).padStart(2, "0")}`;
}
},
methods: {
startTimer() {
this.running = true;
this.intervalID = setInterval(() => {
if (this.remaining > 0) {
this.remaining--;
} else {
clearInterval(this.intervalID);
this.running = false;
}
}, 1000);
},
resetTimer() {
clearInterval(this.intervalID);
this.remaining = this.startSeconds;
this.running = false;
}
},
beforeUnmount() {
clearInterval(this.intervalID);
}
};
</script>
<style scoped>
.timer {
text-align: center;
padding: 24px;
}
.display {
font-size: 3rem;
font-weight: bold;
letter-spacing: 4px;
}
button {
margin: 0 8px;
padding: 8px 20px;
}
</style>
Diagram: SFC Sections Working Together
<template> Displays formattedTime computed value
Calls startTimer and resetTimer methods
Disables Start button via :disabled="running"
<script> Stores remaining, running, intervalID in data()
Computes MM:SS format in formattedTime
startTimer() decrements remaining every second
resetTimer() clears interval and resets state
beforeUnmount() cleans up the interval
<style scoped> Styles only apply to this component's elements
No risk of changing timers in other components
Summary
Single File Components combine a component's template, script, and styles into one .vue file. Vite compiles them into regular JavaScript for the browser. Import components by name and register them in the components option. The scoped attribute on style blocks isolates CSS to the component it belongs to. SFCs are the standard approach for any real Vue project — they keep code organized, styles isolated, and each component self-contained and easy to find.
