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 <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

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 outside setup(), such as named exports consumed by tests.

  • <script setup> — compile-time sugar covered below; the recommended default.

  • <style> — extracted and injected as real stylesheets. Add scoped or module for 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 a render function attached to the component’s options object.

  • <script setup> is transformed into a setup() function; its top-level bindings become the render scope.

  • Each <style> is emitted as a side-effect import so the bundler picks it up; scoped and module rewrite 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 export statements — 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.

  • src cannot 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 a reactive() 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