Inside a Single-File Component
What template, script setup and style compile into, and why the setup attribute changes the meaning of the script block.
<script setup lang="ts"> … </script> <template> … </template> <style scoped> … </style>A single-file component is compiled before it can run. The compiler, @vue/compiler-sfc, turns the file into a standard ES module, and Vite runs it through the Vue plugin whenever a .vue file is imported. Knowing what each block becomes explains most of the rules around them.
The three blocks
<template> holds the markup and compiles into a render function: a function that returns the elements to draw and runs again when the data it read changes. <script> holds the logic and becomes the component object. <style> becomes CSS; with the scoped attribute the compiler adds a data attribute to the elements this file renders and to every selector, so the rules stop at this component.
Why setup changes the script
A plain <script> block is an ordinary module that exports a component object. Adding setup makes the block's body the body of that component's setup() function, which runs once per instance, and exposes every top-level binding — variables, functions, imports — to the template without returning anything. The two forms below are the same component. <script setup> is compile-time shorthand for the longer one.
Macros
defineProps, defineEmits and defineExpose, plus defineOptions and defineSlots from Vue 3.3 and defineModel from 3.4, are compiler macros rather than functions. They are available inside <script setup> with no import and are compiled away. A file may also keep a second, plain <script> block for code that should run once per module instead of once per instance.
Example
<script setup lang="ts">
import { ref } from 'vue'
const title = 'Tasks'
const done = ref(0)
function complete() {
done.value += 1
}
</script>
<template>
<h2>{{ title }}</h2>
<p>{{ done }} done</p>
<button type="button" @click="complete">Complete one</button>
</template>
<style scoped>
h2 {
font-size: 1.25rem;
margin: 0 0 0.5rem;
}
</style>When to use it
- Reading a component written with setup() and recognising it as the same thing as script setup
- Exporting a type or constant from the same file as the component that uses it
- Working out why a style rule in one component is not reaching a child's markup
More examples
The same component in longhand
Identical behaviour, written without the setup attribute: an exported component object with a setup() function that returns the bindings the template needs. This is what <script setup> compiles to, and the return statement is the work it saves. You will see this form in library source, where a component is defined in a .ts file rather than a .vue file.
<script lang="ts">
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const title = 'Tasks'
const done = ref(0)
function complete() {
done.value += 1
}
return { title, done, complete }
},
})
</script>
<template>
<h2>{{ title }}</h2>
<p>{{ done }} done</p>
<button type="button" @click="complete">Complete one</button>
</template>A plain script block beside script setup
One file can hold both blocks. The plain block runs once when the module is first imported and can have named exports, so it is the place for a type or a constant that other files import. The setup block runs once per component instance. Reach for this when a component needs to export something alongside itself.
<script lang="ts">
export type Status = 'draft' | 'published'
export const statuses: Status[] = ['draft', 'published']
</script>
<script setup lang="ts">
import { ref } from 'vue'
const status = ref<Status>('draft')
function publish() {
status.value = 'published'
}
</script>
<template>
<p>Status: {{ status }}</p>
<button type="button" @click="publish">Publish</button>
</template>
Discussion