Updated June 2026. Tested on Vue 3 with <script setup>. Part of the Techalyst Vue 3 series.

A Single File Component, the .vue file, packs a component's markup, logic, and styles into one place. It is how nearly all real Vue is written. Combined with <script setup>, it gives you about the least boilerplate of any component model around. Here is how the pieces fit.

The three blocks

A .vue file has up to three top-level blocks: <template>, <script>, and <style>.

<template>
  <h1>{{ city }}</h1>
</template>

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

<style scoped>
h1 { color: #42b883; }
</style>

None of the blocks is compulsory (a template-only file is valid) and their order is up to you. File names are PascalCase by convention, and the root of an app is conventionally App.vue. When you import a .vue file you get its component definition back, ready to use.

Scoped styles and one gotcha

Add scoped to a <style> block and its CSS applies only to that component's own elements. Vue does this by stamping every element in the template with a unique data attribute and rewriting your selectors to match it.

<style scoped>
p { color: tomato; } /* only this component's <p> tags */
</style>

There is one trap worth knowing. The root element of a child component gets stamped with both its own scope id and its parent's, which means a parent's scoped tag selector can leak onto a child's root element. The fix is to select your wrapper roots by id or class rather than by bare tag name.

<style scoped>
/* avoid: div { padding: 60px } also hits child component roots */
#layout { padding: 60px; } /* safe: targets exactly this element */
</style>

Scoped styles also deliberately do not reach content passed in through a slot, that content belongs to the parent, so style it there.

Why .vue files need a bundler

Browsers cannot read .vue files directly. A build tool compiles them into plain JavaScript, HTML, and CSS first.

.vue files  ->  bundler  ->  JS + CSS the browser understands

In practice that bundler is Vite, which gives you a dev server with instant hot reloading and an optimised production build. You almost never configure the SFC compilation yourself, it is built in.

script setup removes the ceremony

The classic <script> block exports an options object with a setup() function. <script setup> is compile-time sugar that strips away the repetitive parts. Everything you declare at the top level is automatically available to the template, with no return.

<script setup>
import { ref } from 'vue'
const count = ref(0)
function increment() { count.value++ }
</script>

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

Imported components register themselves, no components: {} block needed:

<script setup>
import HelloWorld from './HelloWorld.vue'
</script>

<template>
  <HelloWorld />
</template>

The compiler macros

A handful of compiler macros cover the component interface. They need no import, because the compiler handles them.

<script setup>
const props = defineProps({ message: { type: String, default: 'Hi' } })
const emit = defineEmits(['submit'])

defineExpose({ count }) // allow a parent ref to reach `count`
defineOptions({ name: 'MyWidget', inheritAttrs: false })
</script>

defineProps and defineEmits declare the component's props and events. Because <script setup> components are closed by default (a parent's template ref sees nothing of their internals), defineExpose opts specific values back in. And defineOptions sets component-level options like name and inheritAttrs that used to live in the options object. For non-prop attributes and slots in script, the useAttrs() and useSlots() composables (these you do import) give you what context.attrs and context.slots did.

Need Options setup() <script setup>
Register components components: {} auto on import
Expose to template return {} automatic
Props first setup arg defineProps()
Emits context.emit defineEmits()
Expose to parent context.expose defineExpose()
Component options options object defineOptions()

Wrapping up

A Single File Component keeps template, logic, and style together in one .vue file, with scoped styles for isolation (mind the child-root selector trap, so prefer id selectors on wrappers). A bundler like Vite compiles it for the browser. Reach for <script setup> everywhere: it auto-registers components, exposes top-level bindings to the template, and gives you defineProps, defineEmits, defineExpose, and defineOptions with zero imports. It is the modern default for good reason.

More in the series: Vite for Vue projects and the setup function in Vue 3. Questions welcome below.