Single-File Components
|
This section documents the current Vue 3.x release line as published at
the official Vue.js documentation, which is the reference these pages are written and
verified against. No specific patch version is pinned. The Composition API with This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly. This section’s bibliography lists the reference material consulted while preparing these pages. |
A Vue single-file component (SFC) is a file with a .vue extension that groups one component’s markup,
logic, and styles together. A build step — @vitejs/plugin-vue, wired up by Tooling and Project Setup — compiles it into a plain JavaScript module. See Single-File Components
for the overview and the SFC syntax specification for the exact grammar.
The language blocks
An SFC is a sequence of top-level blocks. A file may contain one <template>, one <script>, one
<script setup> (the two script blocks can coexist), and any number of <style> blocks:
<script setup>
import { ref } from 'vue'
const greeting = ref('Hello')
</script>
<template>
<p class="msg">{{ greeting }}, world</p>
</template>
<style scoped>
.msg {
font-weight: 600;
}
</style>
-
<template>— its contents compile to the component’s render function. Text interpolation uses the\{\{ }}mustache syntax. -
<script>— runs once in module scope when the component is first imported. Use it for code that must execute outsidesetup(), such as named exports consumed by tests. -
<script setup>— compile-time sugar covered below; the recommended default. -
<style>— extracted and injected as real stylesheets. Addscopedormodulefor encapsulation.
Tooling may also define custom blocks (for example <docs> or <i18n>); Vue ignores any block it does
not recognise.
Name inference, src imports, pre-processors, comments
Vue infers the component’s name from the filename, so TodoItem.vue can recurse on itself, and shows a
readable name in warnings and Vue DevTools without an explicit name option
(automatic name inference).
A block can outsource its content with src — useful when migrating existing files:
<script src="./todo-item.js"></script>
<template src="./todo-item.html"></template>
<style src="./todo-item.css"></style>
Any block accepts a lang attribute to opt into a pre-processor — <script lang="ts">,
<template lang="pug">, <style lang="scss"> — as long as the matching package is installed
(pre-processors). Comments inside each block use that
block’s native comment syntax; in <template> that is the HTML comment <!-- … -→.
How an SFC is compiled
@vue/compiler-sfc splits the file into its blocks and emits one JavaScript module
(how it works):
-
<template>becomes arenderfunction attached to the component’s options object. -
<script setup>is transformed into asetup()function; its top-level bindings become the render scope. -
Each
<style>is emitted as a side-effect import so the bundler picks it up;scopedandmodulerewrite the selectors first. -
The dev server adds hot-module-replacement glue and source maps so edits to any block update the running component without a full reload.
<script setup>
Inside <script setup> every top-level binding — imports, variables, functions, and imported components — is available directly in the template, with no return statement
(<script setup>). Reactivity still comes from ref and
reactive.
<script setup>
import { ref } from 'vue'
import BaseIcon from './BaseIcon.vue' // usable as <BaseIcon> below
import { vFocus } from './directives.js' // usable as v-focus below
const open = ref(false)
function toggle() {
open.value = !open.value
}
</script>
<template>
<button v-focus @click="toggle">
<BaseIcon :name="open ? 'chevron-up' : 'chevron-down'" />
</button>
</template>
Compiler macros
These are compile-time only — no import, and they must appear at the top level of <script setup>:
<script setup lang="ts">
// props: typed, with defaults
const props = withDefaults(
defineProps<{ label: string; size?: 'sm' | 'lg' }>(),
{ size: 'sm' },
)
// events: typed emit function
const emit = defineEmits<{ submit: [value: string]; cancel: [] }>()
// two-way binding: parent writes v-model="text", child reads/writes model.value
const model = defineModel<string>()
// expose an imperative API to template refs on the parent
function focus() { /* ... */ }
defineExpose({ focus })
// component-level options that have no other home in <script setup>
defineOptions({ inheritAttrs: false })
// document the slots this component renders
defineSlots<{ default(props: { item: string }): unknown }>()
</script>
defineProps / defineEmits accept either a runtime declaration — defineProps(\{ label: String }) — or,
with lang="ts", a pure type argument as shown. defineModel (stable since 3.4) replaces the old
modelValue prop plus update:modelValue event pair; see
Component v-model.
Top-level await and generics
A top-level await is allowed; the compiled setup() becomes async, so the component must be rendered
under a <Suspense> boundary — see Async Components:
<script setup>
const res = await fetch('/api/config')
const config = await res.json()
</script>
For a generic component, declare the type parameters on the block. They are in scope for defineProps,
defineEmits, and defineSlots:
<script setup lang="ts" generic="T extends { id: number }">
defineProps<{ items: T[]; selected: T | null }>()
const emit = defineEmits<{ pick: [item: T] }>()
</script>
Restrictions
-
No
exportstatements — bindings are exposed to the template implicitly, not exported. -
Macros cannot be called conditionally, in a loop, or from a helper; they must be statically visible at the top level.
-
srccannot point at another file for<script setup>. -
Reactive props destructure (
const \{ size } = defineProps(…), stable since 3.5) is the supported way to destructure props while keeping them reactive; plain destructuring of areactive()object still loses reactivity.
Scoped CSS and other style features
<style scoped> stamps every element in the component with a unique data-v-xxxxxxxx attribute and rewrites
each selector to include a matching attribute selector, so the rules cannot leak to child components
(SFC CSS features). A child component’s root element is
deliberately in scope for both the parent and the child.
<template>
<div class="panel">
<ChildTable />
</div>
</template>
<style scoped>
/* reaches into ChildTable's rendered markup */
.panel :deep(td) {
padding: 4px 8px;
}
/* style content passed into this component's <slot> */
:slotted(a) {
color: teal;
}
/* opt one rule back out of scoping */
:global(body.modal-open) {
overflow: hidden;
}
</style>
<style module> compiles the block as CSS Modules and exposes
the hashed class map to the template as $style (use <style module="cls"> for a different name, or
useCssModule() inside <script setup>):
<template>
<p :class="$style.warning">Careful</p>
</template>
<style module>
.warning { color: #b00; }
</style>
v-bind() in CSS links a style value to component state. Vue compiles it to a CSS custom property that it
keeps updated as the referenced reactive value changes; wrap a JavaScript expression in quotes:
<script setup>
import { ref } from 'vue'
const theme = ref({ accent: '#2f6fa8' })
</script>
<template><span class="tag">new</span></template>
<style scoped>
.tag {
color: v-bind('theme.accent');
}
</style>
See v-bind() in CSS for the custom-property
mechanism and its caveats.
See also
-
Tooling and Project Setup — the Vite plugin and
vue-tscthat compile and type-check.vuefiles. -
Vue and TypeScript — typing props, emits, and generic components in depth.
-
Async Components —
defineAsyncComponent,<Suspense>, and top-levelawait. -
JSX and Rendering and Components — how other frameworks package a component’s template, logic, and styles.