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 scoped attribute 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.

Leave a Comment

Your email address will not be published. Required fields are marked *