| |

Vue.js 16 🟢 Vue Component Basics and Single File Components (.vue SFC)

A Vue application is a tree of components. The root component is mounted to a DOM element, and it renders child components, which render their own children. Each component is a self-contained unit: it has its own template, its own logic, and its own styles. The framework handles the communication between them. This is the component model, and it is the foundation of every Vue application.

This chapter introduces components and the file format that most Vue projects use to define them: the Single File Component, or .vue file. You will learn how a component is structured, how the three blocks of an SFC work together, how components are registered and used, and how the <script setup> syntax makes the component’s script concise.

Key point: A Vue component is a reusable piece of UI with its own state and behavior. A Single File Component is a file with three optional blocks: <template> for the markup, <script> for the logic, and <style> for the styling. The <script setup> block is the modern syntax for the component’s script, and it makes the component’s bindings available to the template automatically.


Why components exist

The reuse problem. A button, a card, a form field, a modal—these appear in many places in an application. Duplicating the markup and logic for each occurrence is repetitive and error-prone. A component encapsulates the markup, the logic, and the styles into a single unit that can be used wherever it is needed. A change to the component changes every instance.

The composition problem. A complex interface is built from simpler pieces. A user profile page contains an avatar component, a name component, a bio component, and a list of posts. Each piece is a component, and the page composes them. The composition is expressed in the template, where the child components are used as custom elements.

The isolation problem. A component’s styles should not leak to the rest of the application. A component’s state should not be shared accidentally. The SFC format provides scoped styles and a module scope for the script, so each component is isolated by default. The isolation is what makes components composable without conflicts.

The tooling problem. The .vue file is a single file that contains the template, the script, and the styles. The build tool (Vite, webpack, or the Vue CLI) parses the file, compiles each block, and produces a JavaScript module that exports the component. The developer writes one file; the tooling handles the rest. The format is the convention, and the tooling is what makes it work.

The template problem. Vue’s template syntax is HTML with directives. The <template> block is parsed by Vue’s compiler, which turns it into a render function. The directives (v-if, v-for, v-model, @click) are the framework’s way of expressing dynamic behavior. The template is not string concatenation; it is a declarative description of the UI.


a. The Single File Component structure

A .vue file has three top-level blocks. All three are optional, but a component without a template renders nothing, and a component without a script has no state or behavior.

<template>
  <div class="greeting">
    <h1>{{ message }}</h1>
    <button @click="count++">Clicked {{ count }} times</button>
  </div>
</template>

<script setup>
import { ref } from 'vue'

const message = 'Hello, Vue!'
const count = ref(0)
</script>

<style scoped>
.greeting {
  padding: 1rem;
  border: 1px solid #ccc;
  border-radius: 4px;
}
</style>

The <template> block is the component’s markup. It contains the HTML that the component renders. The {{ message }} is a text interpolation that displays the value of the message variable. The @click="count++" is an event binding that increments the count ref when the button is clicked.

The <script setup> block is the component’s logic. It imports ref from Vue and declares two bindings: a constant message and a reactive count. The <script setup> syntax makes these bindings available to the template automatically—there is no data() function, no methods object, no return statement. The compiler handles the wiring.

The <style scoped> block is the component’s styles. The scoped attribute limits the styles to this component. Vue adds a unique attribute to the component’s elements and to the selectors, so the styles do not affect other components. Without scoped, the styles are global.

Each block is compiled separately. The template becomes a render function, the script becomes a module, and the styles become a CSS injection. The build tool combines them into a single module that exports the component.


b. <script setup> and the composition API

The <script setup> block is the modern way to write a component’s script. It is a compile-time transform that makes the component’s top-level bindings available to the template.

<script setup>
import { ref, computed } from 'vue'

const firstName = ref('Alice')
const lastName = ref('Smith')

const fullName = computed(() => `${firstName.value} ${lastName.value}`)

function greet() {
  alert(`Hello, ${fullName.value}!`)
}
</script>

<template>
  <p>{{ fullName }}</p>
  <button @click="greet">Greet</button>
</template>

The ref function creates a reactive reference. The computed function creates a derived value. The greet function is a method. All three are available in the template because they are top-level bindings in the <script setup> block.

The template does not need this to access the bindings. In the template, fullName refers to the computed ref, and Vue unwraps it automatically. In the script, fullName.value is required because computed returns a ref.

The <script setup> block can have imports, props, emits, lifecycle hooks, and watchers. It is a complete component script, not a partial one.

<script setup>
import { ref, onMounted } from 'vue'

const props = defineProps({
  initialCount: { type: Number, default: 0 }
})

const emit = defineEmits(['change'])

const count = ref(props.initialCount)

function increment() {
  count.value++
  emit('change', count.value)
}

onMounted(() => {
  console.log('Component mounted')
})
</script>

The defineProps and defineEmits are compiler macros. They are not imported; they are recognized by the compiler and replaced with the appropriate code. The onMounted is a lifecycle hook that runs when the component is mounted.

The <script setup> block is shorter than the Options API equivalent. The Options API uses a data function, a methods object, a computed object, and lifecycle options. The Composition API uses functions and top-level bindings. The two are equivalent in capability; the Composition API is the modern default, and it is what <script setup> uses.


c. Registering and using components

A component is used in another component’s template by importing it and referencing it as a custom element.

<!-- ButtonCounter.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <button @click="count++">Clicked {{ count }} times</button>
</template>
<!-- App.vue -->
<script setup>
import ButtonCounter from './ButtonCounter.vue'
</script>

<template>
  <h1>My App</h1>
  <ButtonCounter />
  <ButtonCounter />
  <ButtonCounter />
</template>

The ButtonCounter component is imported and used three times. Each instance has its own state—its own count ref. The three buttons increment independently. This is the component’s reusability: one definition, many instances, each with its own state.

Component names in the template can be PascalCase (<ButtonCounter />) or kebab-case (<button-counter />). PascalCase is the convention in SFCs because it matches the import name. Kebab-case is required in DOM templates, where the HTML parser lowercases the tag names.

A component can be registered globally, which makes it available in every component’s template without an import.

import { createApp } from 'vue'
import MyComponent from './MyComponent.vue'

const app = createApp({})
app.component('MyComponent', MyComponent)

Global registration is convenient for components that are used everywhere—a button, an icon, a layout component. But it increases the bundle size because the component is included even if it is not used, and it makes the dependency implicit. Local registration with imports is the preferred pattern.

A component can pass data to its children through props. Props are declared in the child with defineProps and passed in the parent’s template.

<!-- Child.vue -->
<script setup>
defineProps({
  title: String,
  count: { type: Number, default: 0 }
})
</script>

<template>
  <h2>{{ title }}</h2>
  <p>Count: {{ count }}</p>
</template>
<!-- Parent.vue -->
<script setup>
import Child from './Child.vue'
</script>

<template>
  <Child title="Hello" :count="42" />
</template>

The parent passes title as a static string and count as a dynamic value. The child declares both as props and uses them in its template. Props are read-only in the child; the child cannot modify them.

A component can emit events to its parent. The child declares the events with defineEmits and calls the emit function. The parent listens with @event-name.

<!-- Child.vue -->
<script setup>
const emit = defineEmits(['increment'])

function handleClick() {
  emit('increment', 1)
}
</script>

<template>
  <button @click="handleClick">+1</button>
</template>
<!-- Parent.vue -->
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'

const count = ref(0)
</script>

<template>
  <Child @increment="count += $event" />
  <p>Count: {{ count }}</p>
</template>

The child emits increment with the value 1. The parent listens for the event and increments count by the emitted value. The $event in the parent’s template is the emitted value.


Complete Example Session

<!-- ============================================ -->
<!-- PART 1: BASIC SFC -->
<!-- ============================================ -->
<template>
  <div>
    <h1>{{ message }}</h1>
  </div>
</template>

<script setup>
const message = 'Hello, Vue!'
</script>
<!-- ============================================ -->
<!-- PART 2: REACTIVE STATE -->
<!-- ============================================ -->
<template>
  <button @click="count++">Clicked {{ count }} times</button>
</template>

<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<!-- ============================================ -->
<!-- PART 3: COMPUTED PROPERTY -->
<!-- ============================================ -->
<template>
  <p>{{ fullName }}</p>
</template>

<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
</script>
<!-- ============================================ -->
<!-- PART 4: METHOD -->
<!-- ============================================ -->
<template>
  <button @click="greet">Greet</button>
</template>

<script setup>
function greet() {
  alert('Hello!')
}
</script>
<!-- ============================================ -->
<!-- PART 5: SCOPED STYLES -->
<!-- ============================================ -->
<template>
  <div class="card">Content</div>
</template>

<style scoped>
.card {
  padding: 1rem;
  border: 1px solid #ccc;
}
</style>
<!-- ============================================ -->
<!-- PART 6: PROPS -->
<!-- ============================================ -->
<!-- Child.vue -->
<script setup>
defineProps({
  title: String,
  count: { type: Number, default: 0 }
})
</script>

<template>
  <h2>{{ title }}</h2>
  <p>Count: {{ count }}</p>
</template>
<!-- ============================================ -->
<!-- PART 7: USING A COMPONENT -->
<!-- ============================================ -->
<!-- Parent.vue -->
<script setup>
import Child from './Child.vue'
</script>

<template>
  <Child title="Hello" :count="42" />
</template>
<!-- ============================================ -->
<!-- PART 8: EMITTING EVENTS -->
<!-- ============================================ -->
<!-- Child.vue -->
<script setup>
const emit = defineEmits(['increment'])
</script>

<template>
  <button @click="emit('increment', 1)">+1</button>
</template>
<!-- ============================================ -->
<!-- PART 9: LISTENING TO EVENTS -->
<!-- ============================================ -->
<!-- Parent.vue -->
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <Child @increment="count += $event" />
  <p>Count: {{ count }}</p>
</template>
<!-- ============================================ -->
<!-- PART 10: LIFECYCLE HOOK -->
<!-- ============================================ -->
<script setup>
import { ref, onMounted } from 'vue'
const data = ref(null)

onMounted(async () => {
  const response = await fetch('/api/data')
  data.value = await response.json()
})
</script>

<template>
  <div v-if="data">{{ data }}</div>
  <div v-else>Loading...</div>
</template>

The ten parts covered a basic SFC, reactive state, a computed property, a method, scoped styles, props, using a component, emitting events, listening to events, and a lifecycle hook.


Quick Reference

SFC Blocks

BlockPurpose
<template>Markup
<script setup>Logic
<style scoped>Scoped styles
<style>Global styles

Script Setup Bindings

BindingAvailable in Template
const x = ref(0)Yes, auto-unwrapped
const y = computed(() => ...)Yes, auto-unwrapped
function fn() {}Yes
import Comp from './Comp.vue'Yes
const plain = 'text'Yes

Compiler Macros

MacroPurpose
defineProps()Declare props
defineEmits()Declare emitted events
defineExpose()Expose bindings to parent
defineModel()Declare a v-model binding

Component Registration

MethodScope
Import in <script setup>Local
app.component('Name', Comp)Global

Props and Events

DirectionMechanism
Parent → ChildProps (defineProps)
Child → ParentEvents (defineEmits)

Best Practices

✅ Do This:

<!-- Use script setup for new components -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>                                                      <!-- ✅ -->

<!-- Use scoped styles to prevent leaks -->
<style scoped>
.card { }                                                      <!-- ✅ -->
</style>                                                       <!-- ✅ -->

<!-- Declare props with types and defaults -->
defineProps({
  count: { type: Number, default: 0 }
})                                                             <!-- ✅ -->

<!-- Declare emitted events -->
defineEmits(['change', 'submit'])                              <!-- ✅ -->

<!-- Use PascalCase for component names -->
import ButtonCounter from './ButtonCounter.vue'                <!-- ✅ -->

<!-- Import components locally -->
<script setup>
import Child from './Child.vue'
</script>                                                      <!-- ✅ -->

❌ Don’t Do This:

<!-- Don't mutate props in the child -->
props.count++                                                  <!-- ❌ -->

<!-- Don't use this in script setup -->
this.count                                                     <!-- ❌ -->

<!-- Don't forget to declare emits -->
emit('change')  // without defineEmits                         <!-- ⚠️ -->

<!-- Don't use global styles for everything -->
<style>
.card { }  // leaks globally                                   <!-- ⚠️ -->
</style>                                                       <!-- ❌ -->

<!-- Don't register every component globally -->
app.component('A', A)
app.component('B', B)  // bundle bloat                          <!-- ⚠️ -->

<!-- Don't use the Options API and Composition API in the same component -->
<script>
export default { data() { return { count: 0 } } }
</script>
<script setup>
const count = ref(0)  // conflict                              <!-- ❌ -->
</script>

Common Pitfalls

PitfallWhy It HappensFix
Binding not in templateNot top-level in <script setup>Move to top level
this is undefined<script setup> has no thisUse direct bindings
Prop mutatedProps are read-onlyEmit an event
Styles leakNo scoped attributeAdd scoped
Component not foundNot importedImport in <script setup>
$event not the valueEvent payloadEmit the value explicitly
Ref not unwrapped in scriptRefs need .value in scriptUse .value in script

Real-World Examples

1. Button Component

<script setup>
defineProps({ label: String })
defineEmits(['click'])
</script>

<template>
  <button @click="$emit('click')">{{ label }}</button>
</template>

2. Card Component

<template>
  <div class="card">
    <slot />
  </div>
</template>

<style scoped>
.card { border: 1px solid #ddd; padding: 1rem; }
</style>

3. User Profile

<script setup>
defineProps({
  user: { type: Object, required: true }
})
</script>

<template>
  <div>
    <h2>{{ user.name }}</h2>
    <p>{{ user.email }}</p>
  </div>
</template>

4. Counter with Emit

<script setup>
import { ref } from 'vue'
const emit = defineEmits(['update'])
const count = ref(0)

function increment() {
  count.value++
  emit('update', count.value)
}
</script>

<template>
  <button @click="increment">{{ count }}</button>
</template>

5. List Component

<script setup>
defineProps({ items: Array })
</script>

<template>
  <ul>
    <li v-for="item in items" :key="item.id">{{ item.name }}</li>
  </ul>
</template>

6. Form Input

<script setup>
const model = defineModel()
</script>

<template>
  <input v-model="model">
</template>

7. Toggle

<script setup>
import { ref } from 'vue'
const open = ref(false)
</script>

<template>
  <button @click="open = !open">Toggle</button>
  <div v-if="open">Content</div>
</template>

8. Modal

<script setup>
defineProps({ open: Boolean })
defineEmits(['close'])
</script>

<template>
  <div v-if="open" class="modal">
    <slot />
    <button @click="$emit('close')">Close</button>
  </div>
</template>

9. Async Data

<script setup>
import { ref, onMounted } from 'vue'
const data = ref(null)

onMounted(async () => {
  const res = await fetch('/api/data')
  data.value = await res.json()
})
</script>

<template>
  <div v-if="data">{{ data }}</div>
  <div v-else>Loading...</div>
</template>

10. Slot Component

<template>
  <div class="layout">
    <header><slot name="header" /></header>
    <main><slot /></main>
  </div>
</template>

Visual

SFC Structure

┌─────────────────────────────────────────────────────────────┐
│  MyComponent.vue                                            │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  <template>                                         │    │
│  │    Markup with directives and bindings              │    │
│  │  </template>                                        │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  <script setup>                                     │    │
│  │    Imports, refs, computed, methods, lifecycle      │    │
│  │  </script>                                          │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  <style scoped>                                     │    │
│  │    Component-specific styles                        │    │
│  │  </style>                                           │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  The build tool compiles each block and combines them       │
│  into a JavaScript module.                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Component Tree

┌─────────────────────────────────────────────────────────────┐
│  App.vue                                                    │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  <Header />                                         │    │
│  └─────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  <UserProfile :user="user">                         │    │
│  │    ┌───────────────────────────────────────────┐    │    │
│  │    │  <Avatar :src="user.avatar" />            │    │    │
│  │    ├───────────────────────────────────────────┤    │    │
│  │    │  <UserName :name="user.name" />           │    │    │
│  │    └───────────────────────────────────────────┘    │    │
│  └─────────────────────────────────────────────────────┘    │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  <PostList :posts="posts" />                        │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  Each component is a node in the tree.                      │
│  Data flows down through props.                             │
│  Events flow up through emits.                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Script Setup Binding

┌─────────────────────────────────────────────────────────────┐
│  <script setup>                                             │
│  import { ref, computed } from 'vue'                        │
│                                                             │
│  const count = ref(0)           ────┐                       │
│  const doubled = computed(...)  ────┤                       │
│  function increment() {}        ────┼──▶ Template           │
│  const message = 'hi'           ────┤    (auto-wired)       │
│  import Child from './Child'    ────┘                       │
│  </script>                                                  │
│                                                             │
│  <template>                                                 │
│    {{ count }}  <!-- auto-unwrapped -->                     │
│    {{ doubled }}                                            │
│    <button @click="increment">                              │
│    <Child />                                                │
│  </template>                                                │
│                                                             │
│  Top-level bindings are available to the template.          │
│  No return statement, no data function, no methods object.  │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Props and Events Flow

┌─────────────────────────────────────────────────────────────┐
│  PARENT                                                     │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  <Child :title="'Hello'" :count="42"                │    │
│  │         @increment="count += $event" />             │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│         │ props down              ▲ events up               │
│         ▼                         │                         │
│                                                             │
│  CHILD                                                      │
│  ┌─────────────────────────────────────────────────────┐    │
│  │  defineProps({ title: String, count: Number })      │    │
│  │  defineEmits(['increment'])                         │    │
│  │                                                     │    │
│  │  emit('increment', 1)                               │    │
│  └─────────────────────────────────────────────────────┘    │
│                                                             │
│  Props are read-only in the child.                          │
│  Events carry data from child to parent.                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘

Summary

ItemValue
Component file.vue Single File Component
Template block<template>
Script block<script setup>
Style block<style scoped>
Reactive stateref()
Derived statecomputed()
PropsdefineProps()
EventsdefineEmits()
LifecycleonMounted(), onUnmounted()
Local registrationImport in <script setup>
Global registrationapp.component()
Auto-unwrapTemplate only

Key takeaways:

  • A component is a reusable piece of UI with its own state and behavior. It is used in another component’s template as a custom element. Each instance has its own state.
  • A Single File Component has three blocks: <template>, <script setup>, and <style scoped>. The template is the markup, the script is the logic, and the style is the component’s CSS. The blocks are compiled separately and combined by the build tool.
  • The <script setup> block is the modern syntax. Top-level bindings are automatically available to the template. There is no data() function, no methods object, no return statement.
  • Reactive state is created with ref(). The template auto-unwraps the ref, but the script requires .value. Computed properties are created with computed() and behave the same way.
  • Props are declared with defineProps() and passed from parent to child. They are read-only in the child. Data flows down.
  • Events are declared with defineEmits() and emitted from child to parent. The parent listens with @event-name. Data flows up.
  • Scoped styles prevent leaks. The scoped attribute on the <style> block limits the styles to the component. Without it, the styles are global.
  • Components are registered locally by importing them in <script setup>. Global registration is available but should be reserved for components that are used everywhere.

Remember: Components are the building blocks of a Vue application. The SFC format is the file that defines them. The three blocks—template, script, and style—are the three concerns of a component: what it looks like, what it does, and how it is styled. The <script setup> block is the modern way to write the script, and it makes the component’s bindings available to the template automatically. Props and events are the communication channels between parent and child. Learn the structure, the bindings, and the two communication mechanisms, and the rest of Vue is composition: components using components, each one small and focused, each one reusable.



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!