skip to content

Single-File Components

A .vue file bundles template, script and style blocks that the SFC compiler turns into a component with scoped CSS. Interviewers probe what the compiler emits and what script setup changes.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

16

What top-level blocks can a Vue single-file component contain, and how many of each does the SFC format allow?

level: juniorimportance: must knowfreq 60%

answer

  1. view, logic, styling, extras
  2. one of each script kind
  3. styles can repeat
  4. custom blocks need tooling

basics

~20 s

A .vue file holds at most one <template>, one <script> and one <script setup>, any number of <style> blocks mixing scoped, module and global, and optional custom blocks such as <docs> that build tooling handles.

solid answer

~40 s

A Vue SFC is an HTML-like file with top-level **language blocks**. `<template>` holds the markup and may appear **at most once**; it is compiled into the component's render function. `<script>` is an ES module whose default export is the component options object, and `<script setup>` is compiled into `setup()`; a file may have **one of each**. `<style>` blocks may appear **several times**, and one file can mix global, `scoped` and `module` blocks. Anything else at the top level is a **custom block**, such as `<docs>` or `<i18n>`, whose meaning is defined by whatever tool handles it. A duplicate `<template>` or `<script>` is a parse error: the compiler reports that a single file component can contain only one such element. Top-level comments use HTML syntax.

code

vue · 21 lines
vue
<script setup lang="ts">
import { ref } from 'vue'
const open = ref(false)
</script>

<template>
  <button class="toggle" @click="open = !open">{{ open ? 'Hide' : 'Show' }}</button>
</template>

<style scoped>
.toggle { padding: 4px 8px; }
</style>

<style>
/* global rule in the same file */
body.modal-open { overflow: hidden; }
</style>

<docs>
A toggle button. Handled only if a docs tool is configured for <docs> blocks.
</docs>

go deeper

for a junior

Recall the three standard blocks, their limits of one template, one script of each kind and many styles, and that custom blocks exist.

for a middle

Explain what each block becomes, how parse() represents them in a descriptor, and that custom blocks depend entirely on build tooling.

for a senior

Use the block rules to judge file organisation, for example when a second style or script block is justified and when a custom block creates hidden tool coupling.

for a principal

Decide whether custom blocks such as i18n or docs belong in components at all, weighing colocation against coupling every component file to specific build plugins.

## What a `.vue` file is A Vue **single-file component** (SFC) is a framework-specific file format, conventionally with the `.vue` extension, that describes one component using an HTML-like syntax. It is syntactically compatible with HTML, but it is not served to the browser as is: `@vue/compiler-sfc` parses it into separate blocks and compiles them into standard JavaScript and CSS. The file is made of **top-level blocks**. Their content is written in the block's own language, and top-level comments between blocks use HTML comment syntax, `<!-- ... -->`. ## The blocks and their limits | Block | How many | What it is for | |---|---|---| | `<template>` | at most one | the markup, compiled into a render function | | `<script>` | at most one | module-scope code; its default export is the component options object | | `<script setup>` | at most one | Composition API code compiled into `setup()` | | `<style>` | any number | CSS; each block may be global, `scoped` or `module` | | custom blocks | any | project-specific content handled by build tooling | Details worth stating precisely: - **Template**: its content is passed to `@vue/compiler-dom`, pre-compiled into a JavaScript render function and attached to the exported component as its `render` option. A component may have no template at all if it provides its own render function. - **Script**: executed as an **ES module**. Its default export should be a component options object, either plain or wrapped in `defineComponent`. - **Script setup**: may coexist with one normal `<script>`; the two count separately, which is why a file can have two script tags but not two of the same kind. - **Style**: several blocks are allowed, and "multiple `<style>` tags with different encapsulation modes can be mixed in the same component", in the words of the SFC specification. - **Custom blocks**: any other top-level tag, for example `<docs>`, `<i18n>` or a GraphQL query block. Vue itself gives them no meaning. ## What the parser enforces `compiler-sfc`'s `parse()` produces a **descriptor** with the fields `template`, `script`, `scriptSetup`, `styles` (an array), and `customBlocks` (an array). The shape mirrors the limits: single slots for the template and each script kind, arrays for styles and custom blocks. A second `<template>` or a second `<script>` of the same kind is reported as a syntax error saying a single file component can contain only one such element. ## Custom blocks in practice Custom blocks are how the format stays extensible: 1. The SFC compiler records each custom block with its tag name, attributes and content. 2. The build integration turns each one into an import of the same `.vue` file with a different request query. 3. A tool-specific plugin or loader must transform that request into JavaScript, for example turning an `<i18n>` block's JSON into translation messages attached to the component. If nothing in the toolchain is configured for a custom block, it contributes nothing to the component, so a custom block is only as useful as the tooling behind it. ## Inspecting the blocks yourself `@vue/compiler-sfc` is re-exported as `vue/compiler-sfc`, so you can look at a descriptor directly, which is a quick way to settle an argument about how a file is read: ```ts import { parse } from 'vue/compiler-sfc' const { descriptor, errors } = parse(source, { filename: 'Toggle.vue' }) descriptor.template?.content // the raw template text descriptor.scriptSetup?.lang // 'ts' for <script setup lang="ts"> descriptor.styles.map((s) => s.scoped) // [true, undefined] for one scoped, one global descriptor.customBlocks.map((b) => b.type) // ['docs'] errors // duplicate-block errors land here ``` Each block records its `type`, `content`, `attrs`, `lang` and `src`, which is all the later compile steps need. ## Two conventions from the specification - **Name inference.** An SFC infers its component name from its **filename** for development warnings, DevTools and recursive self-reference: `FooBar.vue` can render `<FooBar/>` inside itself. - **Block order is free.** Many teams put `<script setup>` first and `<template>` second, but the format does not require any order. ## Summary A `.vue` file is a container of blocks: one template, one script of each kind, any number of styles, and optional custom blocks. The compiler enforces the single-block limits at parse time, and custom blocks are passed on to tooling rather than interpreted by Vue.

  • Can a .vue file contain both <script> and <script setup>, and why would it?
    Yes, one of each. The normal `<script>` runs once in module scope, so it holds named exports or run-once code, while `<script setup>` becomes the per-instance `setup()`. Two blocks of the same kind are rejected by the parser.
  • What happens to a <docs> block if no tool in the build is configured for it?
    Vue gives custom blocks no meaning. The SFC compiler records the block, and the build integration turns it into an import request that a matching plugin or loader is expected to transform; with no such handler it contributes nothing to the component.

An SFC is like a parcel with labelled compartments: one for the drawing (template), one each for the two kinds of instructions (scripts), as many as you like for paint (styles), and spare pockets (custom blocks) that only a courier who knows the label will open.

saying these in an interview costs you the question

  • A .vue file can contain only one <style> block.
  • Two <template> blocks are allowed for conditional layouts.
  • Vue itself interprets custom blocks like <docs> at runtime.
  • A file cannot have both <script> and <script setup>.
  • The browser can load a .vue file directly without compilation.
open as a page

In a Vue single-file component, how does <style scoped> keep CSS local, and which outside elements can its rules still match?

level: juniorimportance: must knowfreq 70%

basics

~20 s

The SFC compiler stamps a per-component data-v-<hash> attribute on the elements its template renders and appends that attribute selector to each scoped rule. A child component's single root element also carries it, so parent rules still reach it.

open as a page

In a Vue 3 <script setup> component, how do top-level variables, functions and imports reach the template, and how often does that code run?

level: juniorimportance: must knowfreq 72%

basics

~20 s

Every top-level binding in <script setup>, including variables, functions and imports, is usable in the template with no return object; imported components are used directly as tags. The block compiles into setup(), so it runs once per component instance.

open as a page

What does a Vue .vue file become after @vue/compiler-sfc and the build step, and what happens to each of its blocks?

level: middleimportance: must knowfreq 55%

basics

~10 s

A .vue file becomes a standard ES module default-exporting a component object: the template becomes a render function, script setup becomes setup(), and styles become plain CSS, injected in development or extracted in production.

open as a page

In Vue 3's <script setup>, why are defineProps, defineEmits and the other define* macros never imported, and what restrictions follow from that?

level: middleimportance: must knowfreq 58%

basics

~20 s

The define* macros are compiler hints that @vue/compiler-sfc recognises and compiles away, so they need no import. Their arguments are hoisted to module scope, so they cannot reference variables declared in <script setup>, and they only work inside <script setup>.

open as a page

In a Vue SFC with <style scoped>, your rule for a third-party date picker's inner .dp-cell never applies; why, and how do you fix it without going global?

level: seniorimportance: must knowfreq 62%

basics

~10 s

Your rule compiles to .dp-cell[data-v-yours], but the picker's inner cells carry the library's attribute or none, so it never matches. Write .picker-wrap :deep(.dp-cell), which compiles to .picker-wrap[data-v-yours] .dp-cell and stays under your wrapper.

open as a page

In a Vue single-file component, how do you write a block in TypeScript, SCSS or Pug, and what actually processes the lang attribute?

level: juniorimportance: should knowfreq 45%

basics

~20 s

Add a lang attribute to the block, such as <script setup lang="ts">, <style lang="scss"> or <template lang="pug">. The SFC compiler hands style and template content to the matching preprocessor, which you install, and leaves TypeScript for the toolchain to transpile.

open as a page

Why do Vue single-file components need a build step, and what changes when you use Vue without one and compile templates in the browser?

level: middleimportance: should knowfreq 50%

basics

~20 s

Browsers cannot load .vue files, so @vue/compiler-sfc must compile them ahead of time. Without a build, templates are strings or in-DOM markup compiled in the browser: the compiler ships to users, and script setup and scoped styles disappear.

open as a page

In a Vue 3 component with <style scoped>, why do its rules not style the content a parent passes into its <slot>, and what does :slotted() change?

level: middleimportance: should knowfreq 38%

basics

~20 s

Slot content is compiled in the parent's template, so its elements carry the parent's data-v attribute, not the child's. :slotted(.x) compiles to .x[data-v-child-s], and Vue adds that -s attribute to whatever the child renders through its <slot>.

open as a page

In a Vue SFC, how does v-bind() inside a <style> block make CSS follow component state, and what does it compile to?

level: middleimportance: should knowfreq 45%

basics

~20 s

Each v-bind() becomes var(--<hashed-name>) in static CSS, and the component sets that custom property as an inline style on its root element, updating it when the bound state changes. Expressions must be quoted, as in v-bind('theme.color').

open as a page

In a Vue SFC, when do you still need a normal <script> block beside <script setup>, and what rules govern combining the two?

level: middleimportance: should knowfreq 45%

basics

~20 s

Use a normal <script> beside <script setup> for named exports, module-scope code that must run once, and, before Vue 3.3, options like inheritAttrs. Both blocks must share the same lang, and props or emits never go in the normal block.

open as a page

In Vue 3, what does it mean that a <script setup> component is closed by default, and what does a parent's template ref to it actually see?

level: middleimportance: should knowfreq 50%

basics

~20 s

A <script setup> component exposes none of its bindings on its public instance, so a parent's template ref or $parent sees only built-in properties like $el and $props. Bindings become visible only through the defineExpose macro.

open as a page

When migrating a Vue 3 component from an explicit setup() function to <script setup>, what changes mechanically, and what typically breaks for its callers?

level: seniorimportance: should knowfreq 42%

basics

~20 s

The setup body moves to the top level: the return object goes, props and emits become macros, components and directives become imports. What breaks is outside access: parents calling child methods through refs, and Options API code reading setup state.

open as a page

In a Vue SFC, how do you use a <style module> block from the template and from <script setup>, and how does it differ from scoped?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

A <style module> block hashes its class names and exposes a map as $style in the template, or under a custom name with module="classes". In script setup, useCssModule() or useCssModule('classes') returns the same map. Scoped keeps class names and adds attributes.

open as a page

In Vue's <script setup>, what does a top-level await compile to, and why does the compiler rewrite each such await?

level: middleimportance: nice to knowfreq 28%

basics

~20 s

A top-level await makes the compiled setup() an async setup(). The compiler wraps each top-level awaited expression with Vue's withAsyncContext helper so the current component instance is restored after the await, letting hooks and watchers registered later still attach to the component.

open as a page

Should a large Vue SFC be split into separate template, script and style files using src imports, and what does that cost?

level: seniorimportance: nice to knowfreq 22%

basics

~20 s

Usually not. src loads a block from another file, but <script setup> cannot use src, nor can a normal <script> beside it, so you lose script setup. Colocation is the point: decompose into smaller components and composables instead.

open as a page