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.
All comments ()
No comments yet
Be the first to leave a comment on this post.