<script setup>
<script setup> is a compile-time syntactic sugar for using Composition API inside Single-File Components (SFCs). It is the recommended syntax if you are using both SFCs and Composition API. It provides a number of advantages over the normal <script> syntax:
- More succinct code with less boilerplate
- Ability to declare props and emitted events using pure TypeScript
- Better runtime performance (the template is compiled into a render function in the same scope, without an intermediate proxy)
- Better IDE type-inference performance (less work for the language server to extract types from code)
Basic Syntax
To opt-in to the syntax, add the setup attribute to the <script> block:
vue
<script setup>
console.log('hello script setup')
</script>The code inside is compiled as the content of the component's setup() function. This means that unlike normal <script>, which only executes once when the component is first imported, code inside <script setup> will execute every time an instance of the component is created.
Top-level bindings are exposed to template
When using <script setup>, any top-level bindings (including variables, function declarations, and imports) declared inside <script setup> are directly usable in the template:
vue
<script setup>
// variable
const msg = 'Hello!'
// functions
function log() {
console.log(msg)
}
</script>
<template>
<button @click="log">{{ msg }}</button>
</template>Imports are exposed in the same fashion. This means you can directly use an imported helper function in template expressions without having to expose it via the methods option:
vue
<script setup>
import { capitalize } from './helpers'
</script>
<template>
<div>{{ capitalize('hello') }}</div>
</template>Reactivity
Reactive state needs to be explicitly created using Reactivity APIs. Similar to values returned from a setup() function, refs are automatically unwrapped when referenced in templates:
vue
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>Using Components
Values in the scope of <script setup> can also be used directly as custom component tag names:
vue
<script setup>
import MyComponent from './MyComponent.vue'
</script>
<template>
<MyComponent />
</template>Think of MyComponent as being referenced as a variable. If you have used JSX, the mental model is similar here. The kebab-case equivalent <my-component> also works in the template - however PascalCase component tags are strongly recommended for consistency. It also helps differentiating from native custom elements.
Dynamic Components
Since components are referenced as variables instead of registered under string keys, we should use dynamic :is binding when using dynamic components inside <script setup>:
vue
<script setup>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
</script>
<template>
<component :is="Foo" />
<component :is="someCondition ? Foo : Bar" />
</template>Note how the components can be used as variables in a ternary expression.
Recursive Components
An SFC can implicitly refer to itself via its filename. E.g. a file named FooBar.vue can refer to itself as <FooBar/> in its template.
Note this has lower priority than imported components. If you have a named import that conflicts with the component's inferred name, you can alias the import:
js
import { FooBar as FooBarChild } from './components'Namespaced Components
You can use component tags with dots like <Foo.Bar> to refer to components nested under object properties. This is useful when you import multiple components from a single file:
vue
<script setup>
import * as Form from './form-components'
</script>
<template>
<Form.Input>
<Form.Label>label</Form.Label>
</Form.Input>
</template>Using Custom Directives
Globally registered custom directives just work as normal. Local custom directives don't need to be explicitly registered with <script setup>, but they must follow the naming scheme vNameOfDirective:
vue
<script setup>
const vMyDirective = {
beforeMount: (el) => {
// do something with the element
}
}
</script>
<template>
<h1 v-my-directive>This is a Heading</h1>
</template>If you're importing a directive from elsewhere, it can be renamed to fit the required naming scheme:
vue
<script setup>
import { myDirective as vMyDirective } from './MyDirective.js'
</script>defineProps() & defineEmits()
To declare options like props and emits with full type inference support, we can use the defineProps and defineEmits APIs, which are automatically available inside <script setup>:
vue
<script setup>
const props = defineProps({
foo: String
})
const emit = defineEmits(['change', 'delete'])
// setup code
</script>definePropsanddefineEmitsare compiler macros only usable inside<script setup>. They do not need to be imported, and are compiled away when<script setup>is processed.definePropsaccepts the same value as thepropsoption, whiledefineEmitsaccepts the same value as theemitsoption.definePropsanddefineEmitsprovide proper type inference based on the options passed.The options passed to
definePropsanddefineEmitswill be hoisted out of setup into module scope. Therefore, the options cannot reference local variables declared in setup scope. Doing so will result in a compile error. However, it can reference imported bindings since they are in the module scope as well.
Type-only props/emit declarations
Props and emits can also be declared using pure-type syntax by passing a literal type argument to defineProps or defineEmits:
ts
const props = defineProps<{
foo: string
bar?: number
}>()
const emit = defineEmits<{
(e: 'change', id: number): void
(e: 'update', value: string): void
}>()
// 3.3+: alternative, more succinct syntax
const emit = defineEmits<{
change: [id: number] // named tuple syntax
update: [value: string]
}>()definePropsordefineEmitscan only use either runtime declaration OR type declaration. Using both at the same time will result in a compile error.When using type declaration, the equivalent runtime declaration is automatically generated from static analysis to remove the need for double declaration and still ensure correct runtime behavior.
In dev mode, the compiler will try to infer corresponding runtime validation from the types. For example here
foo: Stringis inferred from thefoo: stringtype. Imported types are also resolved, provided TypeScript is installed as a peer dependency.In prod mode, the compiler will generate the array format declaration to reduce bundle size (the props here will be compiled into
['foo', 'bar'])
In version 3.2 and below, the generic type parameter for
defineProps()were limited to a type literal or a reference to a local interface.This limitation was resolved in 3.3. The latest version of Vue supports referencing imported and a limited set of complex types in the type parameter position. However, because the type to runtime conversion is still AST-based, some complex types that require actual type analysis, e.g. conditional types, are not supported. You can use conditional types for the type of a single prop, but not the entire props object.
Reactive Props Destructure
In Vue 3.5 and above, variables destructured from the return value of defineProps are reactive. Vue's compiler automatically prepends props. when code in the same <script setup> block accesses variables destructured from defineProps:
ts
const { foo } = defineProps(['foo'])
watchEffect(() => {
// runs only once before 3.5
// re-runs when the "foo" prop changes in 3.5+
console.log(foo)
})The above is compiled to the following equivalent:
js
const props = defineProps(['foo'])
watchEffect(() => {
// `foo` transformed to `props.foo` by the compiler
console.log(props.foo)
})In addition, you can use JavaScript's native default value syntax to declare default values for the props. This is particularly useful when using the type-based props declaration:
ts
interface Props {
msg?: string
labels?: string[]
}
const { msg = 'hello', labels = ['one', 'two'] } = defineProps<Props>()Default props values when using type declaration
In 3.5 and above, default values can be naturally declared when using Reactive Props Destructure. But in 3.4 and below, Reactive Props Destructure is not enabled by default. In order to declare props default values with type-based declaration, the withDefaults compiler macro is needed:
ts
interface Props {
msg?: string
labels?: string[]
}
const props = withDefaults(defineProps<Props>(), {
msg: 'hello',
labels: () => ['one', 'two']
})This will be compiled to equivalent runtime props default options. In addition, the withDefaults helper provides type checks for the default values, and ensures the returned props type has the optional flags removed for properties that do have default values declared.
INFO
Note that default values for mutable reference types (like arrays or objects) should be wrapped in functions when using withDefaults to avoid accidental modification and external side effects. This ensures each component instance gets its own copy of the default value. This is not necessary when using default values with destructure.
defineModel()
- Only available in 3.4+
This macro can be used to declare a two-way binding prop that can be consumed via v-model from the parent component. Example usage is also discussed in the Component v-model guide.
Under the hood, this macro declares a model prop and a corresponding value update event. If the first argument is a literal string, it will be used as the prop name; Otherwise the prop name will default to "modelValue". In both cases, you can also pass an additional object which can include the prop's options and the model ref's value transform options.
js
// declares "modelValue" prop, consumed by parent via v-model
const model = defineModel()
// OR: declares "modelValue" prop with options
const model = defineModel({ type: String })
// emits "update:modelValue" when mutated
model.value = 'hello'
// declares "count" prop, consumed by parent via v-model:count
const count = defineModel('count')
// OR: declares "count" prop with options
const count = defineModel('count', { type: Number, default: 0 })
function inc() {
// emits "update:count" when mutated
count.value++
}WARNING
If you have a default value for defineModel prop and you don't provide any value for this prop from the parent component, it can cause a de-synchronization between parent and child components. In the example below, the parent's myRef is undefined, but the child's model is 1:
vue
<script setup>
const model = defineModel({ default: 1 })
</script>vue
<script setup>
const myRef = ref()
</script>
<template>
<Child v-model="myRef"></Child>
</template>Also, when using withDefaults with defineProps, default values for mutable reference types (like arrays or objects) should be wrapped in functions in defineModel to avoid accidental modification and external side effects.
Modifiers and Transformers
To access modifiers used with the v-model directive, we can destructure the return value of defineModel() like this:
js
const [modelValue, modelModifiers] = defineModel()
// corresponds to v-model.trim
if (modelModifiers.trim) {
// ...
}When a modifier is present, we likely need to transform the value when reading or syncing it back to the parent. We can achieve this by using the get and set transformer options:
js
const [modelValue, modelModifiers] = defineModel({
// get() omitted as it is not needed here
set(value) {
// if the .trim modifier is used, return trimmed value
if (modelModifiers.trim) {
return value.trim()
}
// otherwise, return the value as-is
return value
}
})Usage with TypeScript
Like defineProps and defineEmits, defineModel can also receive type arguments to specify the types of the model value and the modifiers:
ts
const modelValue = defineModel<string>()
// ^? Ref<string | undefined>
// default model with options, required removes possible undefined values
const modelValue = defineModel<string>({ required: true })
// ^? Ref<string>
const [modelValue, modifiers] = defineModel<string, 'trim' | 'uppercase'>()
// ^? Record<'trim' | 'uppercase', true | undefined>defineExpose()
Components using <script setup> are closed by default - i.e. the public instance of the component, which is retrieved via template refs or $parent chains, will not expose any of the bindings declared inside <script setup>.
To explicitly expose properties in a <script setup> component, use the defineExpose compiler macro:
vue
<script setup>
import { ref } from 'vue'
const a = 1
const b = ref(2)
defineExpose({
a,
b
})
</script>When a parent gets an instance of this component via template refs, the retrieved instance will be of the shape { a: number, b: number } (refs are automatically unwrapped just like on normal instances).
defineOptions()
- Only supported in 3.3+
This macro can be used to declare component options directly inside <script setup> without having to use a separate <script> block:
vue
<script setup>
defineOptions({
inheritAttrs: false,
customOptions: {
/* ... */
}
})
</script>- This is a macro. The options will be hoisted to module scope and cannot access local variables in
<script setup>that are not literal constants.
defineSlots()
- Only supported in 3.3+
This macro can be used to provide type hints to IDEs for slot name and props type checking.
defineSlots() only accepts a type parameter and no runtime arguments. The type parameter should be a type literal where the property key is the slot name, and the value type is the slot function. The first argument of the function is the props the slot expects to receive, and its type will be used for slot props in the template. The return type is ignored by default and can be any. Experimental tooling can use it to check slot children.
It also returns the slots object, which is equivalent to the slots object exposed on the setup context or returned by useSlots().
vue
<script setup lang="ts">
const slots = defineSlots<{
default(props: { msg: string }): any
}>()
</script>Restricting Slot Children (Experimental)
Draft proposal
This section describes the experimental implementation of RFC 734, targeting a future minor release of Vue Language Tools. It requires a build containing that implementation; it is not available in current stable tooling. The option and helper types may change before the RFC is accepted.
Slot return types can describe which children a component accepts. Enable the experiment explicitly in tsconfig.json:
json
{
"compilerOptions": { "strict": true },
"vueCompilerOptions": { "strictSlotChildren": true }
}strictTemplates does not enable this experiment. Both the Vue language extension and vue-tsc must use a version that supports it.
For example, a generic Tabs.vue can accept only TabItem children with matching value types:
vue
<script setup lang="ts" generic="T">
import type TabItem from './TabItem.vue'
import type { Renders } from 'vue-component-type-helpers'
defineProps<{ selected: T }>()
defineSlots<{
default(): Renders<typeof TabItem<T>>[]
}>()
</script>
<template>
<slot />
</template>Here TabItem.vue declares generic="T" and defineProps<{ value: T }>(). A consumer can write:
vue
<Tabs :selected="1">
<TabItem :value="1" />
<TabItem :value="2" />
</Tabs>
<!-- Type errors: the value type and the native child do not match. -->
<Tabs :selected="1">
<TabItem value="wrong" />
<input />
</Tabs>The slot type can also be an imported interface, a re-exported type alias, or a generic type. TypeScript resolves these types, including component generic arguments; they do not need to be written inline.
Child Types and Counts
The return type describes the rendered children, with template fragments flattened:
| Return type | Accepted content |
|---|---|
HTMLInputElement | One input element |
HTMLInputElement[] | Zero or more input elements |
readonly [HTMLInputElement, HTMLButtonElement] | An input followed by a button |
Renders<typeof TabItem<number>> | One compatible TabItem with a numeric value |
Renders<typeof TabItem<number>>[] | Zero or more compatible items |
string or Text | One rendered text node, including interpolation |
HTMLInputElement | undefined | An input or no rendered children |
[] | No rendered children |
VNode[] | Any number of VNodes, including text VNodes |
any or unknown | Unrestricted content |
Comments and formatting whitespace do not count as children. Adjacent text and interpolations form one text node. A native element's descendants belong to that element, not to the surrounding slot.
Each v-if branch must satisfy the constraint, including the empty branch when there is no v-else. A v-for can render zero or many children, so it cannot satisfy a required single child or a fixed-length tuple. Named slots are checked independently. An optional slot may be omitted; if explicitly provided, its content must satisfy its return type. Required slots whose return types exclude empty content must be provided on every branch.
For dynamic slot names and dynamic components, the content must satisfy every possible slot contract. A dynamic name supplies one of its possible names, rather than all of them. Conditional named slots retain which names are present together on each branch.
Component Unions and Bound Props
Use Renders<Component, Props> from vue-component-type-helpers to describe component render types. The optional second argument constrains the props actually passed to the child:
ts
import type { Renders } from 'vue-component-type-helpers'
import type TabItem from './TabItem.vue'
import type TabSeparator from './TabSeparator.vue'
defineSlots<{
default(): Renders<typeof TabItem<number> | typeof TabSeparator>[]
selected(): Renders<typeof TabItem<number>, { selected: true }>
}>()Renders<typeof A | typeof B> also accepts the spelling Renders<typeof A> | Renders<typeof B>. Each component remains associated with its own props. (Renders<typeof A> | Renders<typeof B>)[] allows mixed children; Renders<typeof A>[] | Renders<typeof B>[] requires all children to belong to one alternative. Unions of tuples preserve their order and length constraints.
An optional prop declaration alone does not satisfy a required bound prop. Renders<unknown, { value: number }> accepts any component receiving a numeric value prop. The helper describes types for tooling; slot functions still return VNodes at runtime.
Relationship to Flow's renders
Like Flow's render types, this lets a component specify which components it accepts as children. Renders<typeof Comp> corresponds conceptually to renders Comp, Renders<typeof Comp> | undefined to renders? Comp, and Renders<typeof Comp>[] to renders* Comp. TypeScript keeps its existing generic syntax; no new keyword is needed.
The tooling also infers the roots of SFC wrappers. A wrapper rendering TabItem can be accepted where a TabItem is expected, including generic wrappers and wrappers with multiple roots. A wrapper containing <div><TabItem /></div> renders a div at its root, so it does not satisfy a TabItem constraint.
SFCs checked with this option carry a distinct component identity in their generated types. This prevents unrelated SFCs with identical props from satisfying each other's render constraints. Wrappers retain that identity across imports and declaration output. Components without this metadata and native DOM interfaces follow TypeScript's structural assignability rules.
For a wrapper forwarding a slot, the checker follows the children supplied by its caller and uses the fallback when the slot is absent or empty. Every possible conditional branch must satisfy the constraint. Wrapper cycles that cannot establish the requested render type are rejected; finite wrapper chains are not limited by a fixed wrapper-depth setting. As with other TypeScript types, sufficiently complex expressions can reach TypeScript's own instantiation limits.
Root inference requires an SFC template checked with this option. Arbitrary render functions do not expose their rendered roots through their usual VNode return type. This feature checks template composition at development time and adds no runtime validation.
useSlots() & useAttrs()
Usage of slots and attrs inside <script setup> should be relatively rare, since you can access them directly as $slots and $attrs in the template. In the rare case where you do need them, use the useSlots and useAttrs helpers respectively:
vue
<script setup>
import { useSlots, useAttrs } from 'vue'
const slots = useSlots()
const attrs = useAttrs()
</script>useSlots and useAttrs are actual runtime functions that return the equivalent of setupContext.slots and setupContext.attrs. They can be used in normal composition API functions as well.
Usage alongside normal <script>
<script setup> can be used alongside normal <script>. A normal <script> may be needed in cases where we need to:
- Declare options that cannot be expressed in
<script setup>, for exampleinheritAttrsor custom options enabled via plugins (Can be replaced bydefineOptionsin 3.3+). - Declaring named exports.
- Run side effects or create objects that should only execute once.
vue
<script>
// normal <script>, executed in module scope (only once)
runSideEffectOnce()
// declare additional options
export default {
inheritAttrs: false,
customOptions: {}
}
</script>
<script setup>
// executed in setup() scope (for each instance)
</script>Support for combining <script setup> and <script> in the same component is limited to the scenarios described above. Specifically:
- Do NOT use a separate
<script>section for options that can already be defined using<script setup>, such aspropsandemits. - Variables created inside
<script setup>are not added as properties to the component instance, making them inaccessible from the Options API. Mixing APIs in this way is strongly discouraged.
If you find yourself in one of the scenarios that is not supported then you should consider switching to an explicit setup() function, instead of using <script setup>.
Top-level await
Top-level await can be used inside <script setup>. The resulting code will be compiled as async setup():
vue
<script setup>
const post = await fetch(`/api/post/1`).then((r) => r.json())
</script>In addition, the awaited expression will be automatically compiled in a format that preserves the current component instance context after the await.
Note
async setup() must be used in combination with Suspense, which is currently still an experimental feature. We plan to finalize and document it in a future release - but if you are curious now, you can refer to its tests to see how it works.
Import Statements
Import statements in vue follow ECMAScript module specification. In addition, you can use aliases defined in your build tool configuration:
vue
<script setup>
import { ref } from 'vue'
import { componentA } from './Components'
import { componentB } from '@/Components'
import { componentC } from '~/Components'
</script>Generics
Generic type parameters can be declared using the generic attribute on the <script> tag:
vue
<script setup lang="ts" generic="T">
defineProps<{
items: T[]
selected: T
}>()
</script>The value of generic works exactly the same as the parameter list between <...> in TypeScript. For example, you can use multiple parameters, extends constraints, default types, and reference imported types:
vue
<script
setup
lang="ts"
generic="T extends string | number, U extends Item"
>
import type { Item } from './types'
defineProps<{
id: T
list: U[]
}>()
</script>You can use @vue-generic the directive to pass in explicit types, for when the type cannot be inferred:
vue
<template>
<!-- @vue-generic {import('@/api').Actor} -->
<ApiSelect v-model="peopleIds" endpoint="/api/actors" id-prop="actorId" />
<!-- @vue-generic {import('@/api').Genre} -->
<ApiSelect v-model="genreIds" endpoint="/api/genres" id-prop="genreId" />
</template>In order to use a reference to a generic component in a ref you need to use the vue-component-type-helpers library as InstanceType won't work.
vue
<script
setup
lang="ts"
>
import componentWithoutGenerics from '../component-without-generics.vue';
import genericComponent from '../generic-component.vue';
import type { ComponentExposed } from 'vue-component-type-helpers';
// Works for a component without generics
ref<InstanceType<typeof componentWithoutGenerics>>();
ref<ComponentExposed<typeof genericComponent>>();Restrictions
- Due to the difference in module execution semantics, code inside
<script setup>relies on the context of an SFC. When moved into external.jsor.tsfiles, it may lead to confusion for both developers and tools. Therefore,<script setup>cannot be used with thesrcattribute. <script setup>does not support In-DOM Root Component Template.(Related Discussion)