skip to content

SFC Blocks & Compilation

An SFC holds template, script, style and custom blocks that the compiler turns into a component object with a render function and scoped CSS. Interviewers ask why that needs a build step.

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

explore

questions

5

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

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 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

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