Vue (vue) is the progressive JavaScript framework for building modern web UI. This guide covers everything you need to use Vue in your applications: installing it, creating reactive state, building components, wiring up the template syntax, rendering on the server, and shipping framework-grade widgets as native custom elements.
This document is built directly from the source of the vuejs/core monorepo at version 3.5.35. Written from the package entry points and implementation. Where Vue exposes the same function from several packages, this guide tells you which import path to use.
Note
vuejs/coreis the home of the core Vue runtime and compiler. It is published to npm as thevuepackage, plus a family of scoped packages (@vue/reactivity,@vue/runtime-core,@vue/runtime-dom,@vue/server-renderer,@vue/compiler-sfc, and more). When this guide says "import fromvue", it refers to the umbrella package that re-exports the runtime + compiler for browser apps.
1. Quick Start
The fastest way to a running Vue app is the official scaffolding tool, which sets up Vite, single-file components, and sensible defaults.
npm create vue@latest
Vue.js - The Progressive JavaScript Framework
✔ Project name: … my-app
✔ Add TypeScript? … No / Yes
✔ Add JSX Support? … No / Yes
✔ Add Vue Router for Single Page Application development? … No / Yes
✔ Add Pinia for state management? … No / Yes
...
Scaffolding project in ./my-app...
Done.
Then install and start the dev server:
cd my-app
npm install
npm run dev
VITE v8.0.0 ready in 312 ms
➜ Local: http://localhost:5173/
➜ Network: use --host to expose
If you just want to try Vue on a single HTML page with no build step, load the global build from a CDN and call createApp:
<div id="app">{{ message }}</div>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp, ref } = Vue
createApp({
setup() {
const message = ref('Hello Vue!')
return { message }
},
}).mount('#app')
</script>
Tip
The CDN snippet above uses the full build (
vue.global.js), which includes the template compiler so that the in-DOM{{ message }}template can be compiled in the browser. For production apps with a build step, you use the runtime-only build, where templates are compiled ahead of time. See Choosing a Build.
2. Installation
2.1 Install from npm
npm install vue
added 1 package in 1s
Vue ships as an ES module and a CommonJS module, with TypeScript declarations included. The published vue package depends on the internal Vue packages (@vue/runtime-dom, @vue/compiler-dom, @vue/compiler-sfc, @vue/server-renderer, @vue/shared), so a single install brings the whole framework.
Note
Vue's npm package marks
typescriptas an optional peer dependency. You do not need TypeScript to use Vue, but if you have it installed, Vue's single-file-component tooling can use it for type-aware compilation of<script setup lang="ts">.
2.2 Choosing a build
Vue publishes several build formats from the same source. The right one depends on whether you use a bundler and whether you compile templates ahead of time.
| Build file | Format | Includes compiler? | Use when |
|---|---|---|---|
vue.runtime.esm-bundler.js |
ESM (bundler) | No | Default for Vite/webpack/Rollup apps with SFCs (the module entry) |
vue.esm-bundler.js |
ESM (bundler) | Yes | Bundler apps that compile templates at runtime |
vue.esm-browser.js |
ESM (browser) | Yes | Native <script type="module"> with no bundler |
vue.runtime.esm-browser.js |
ESM (browser) | No | Native ESM, templates precompiled |
vue.global.js |
IIFE (window.Vue) |
Yes | Plain <script> tag, dev |
vue.global.prod.js |
IIFE, minified | Yes | Plain <script> tag, production |
vue.cjs.js / vue.cjs.prod.js |
CommonJS | Yes | Node.js require() (SSR, tests) |
Important
The package's default
importentry resolves tovue.runtime.esm-bundler.js— the runtime-only build. This is intentional: in a real app your.vuesingle-file components are compiled to render functions at build time, so the in-browser compiler is dead weight. If you pass a stringtemplateoption at runtime with a runtime-only build, Vue cannot compile it and warns you (see Troubleshooting).
2.3 Bundler feature flags
When you use a bundler build, Vue exposes three global compile-time flags you should define for the smallest, most correct production bundle. Define them in your bundler config (Vite's @vitejs/plugin-vue sets sensible defaults automatically):
| Flag | Default | Effect |
|---|---|---|
__VUE_OPTIONS_API__ |
true |
Set to false to tree-shake the Options API if you use only Composition API |
__VUE_PROD_DEVTOOLS__ |
false |
Set to true to enable devtools support in production builds |
__VUE_PROD_HYDRATION_MISMATCH_DETAILS__ |
false |
Set to true to get detailed hydration-mismatch warnings in production |
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
define: {
__VUE_OPTIONS_API__: 'false',
__VUE_PROD_DEVTOOLS__: 'false',
},
})
Tip
Turning off
__VUE_OPTIONS_API__removes the Options-API compatibility layer from your bundle. Only do this if every component in your app (and your dependencies) uses the Composition API exclusively.
2.4 System requirements
| Requirement | Value |
|---|---|
| Node.js (for tooling/SSR) | >= 20.0.0 |
| Browser target (default builds) | ES2016+ (modern evergreen browsers) |
| TypeScript (optional) | ~5.6 recommended for the SFC type macros |
3. Creating an Application
Every Vue app starts with createApp (or createSSRApp for server-rendered apps), imported from vue.
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
createApp returns an application instance. You configure the app on this instance before calling .mount().
3.1 The application instance API
| Method | Purpose |
|---|---|
app.mount(rootContainer) |
Render the root component into a DOM element or CSS selector string. Returns the root component instance. |
app.unmount() |
Tear down the mounted app. |
app.component(name, component?) |
Register (or retrieve) a global component. |
app.directive(name, directive?) |
Register (or retrieve) a global custom directive. |
app.use(plugin, ...options) |
Install a plugin. |
app.mixin(mixin) |
Apply a global mixin (Options API). |
app.provide(key, value) |
Provide a value to all descendant components via injection. |
app.runWithContext(fn) |
Run a callback with the app as the active injection context. |
app.config |
The app-level configuration object (see below). |
app.version |
The Vue version string this app was created with. |
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import MyButton from './components/MyButton.vue'
const app = createApp(App)
app.use(router) // install a plugin
app.component('MyButton', MyButton) // global component
app.provide('apiBase', '/api/v1') // app-level injection
app.mount('#app')
Important
app.mount()clears the existing content of the target container before rendering. Anything already inside#appis removed (except when you intentionally use in-DOM templates with the full build).
3.2 Application configuration (app.config)
| Config key | Purpose |
|---|---|
errorHandler |
Global handler for uncaught errors from components, watchers, and lifecycle hooks. |
warnHandler |
Custom handler for dev-mode warnings. |
performance |
Set true to enable component init/render/patch performance markers in the browser timeline. |
compilerOptions |
Options passed to the in-browser template compiler (full build only). |
globalProperties |
An object whose properties are accessible on every component instance (e.g. app.config.globalProperties.$http). |
optionMergeStrategies |
Custom merge strategies for custom component options. |
app.config.errorHandler = (err, instance, info) => {
// report to your error tracking service
console.error('Vue error:', err, info)
}
app.config.globalProperties.$formatDate = (d) => new Date(d).toLocaleDateString()
Caution
app.config.isCustomElementis deprecated. To tell Vue which tags are custom elements, useapp.config.compilerOptions.isCustomElementinstead. With a runtime-only build,isCustomElementmust be configured in your build tool (e.g.@vitejs/plugin-vue), because there is no runtime compiler to read the option.
4. Reactivity
Reactivity is the heart of Vue. The reactivity APIs are exported from both vue and the standalone @vue/reactivity package, so you can use them in plain JavaScript with no components at all.
4.1 Refs
A ref is a reactive container for a single value. Read and write it through its .value property.
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
0
1
In templates, refs are unwrapped automatically — you write {{ count }}, not {{ count.value }}.
| Function | Purpose |
|---|---|
ref(value) |
Create a deeply-reactive ref. Objects assigned to a ref are made reactive. |
shallowRef(value) |
Create a ref that only tracks .value reassignment, not deep mutations. |
isRef(r) |
Return true if a value is a ref. |
unref(r) |
Return r.value if r is a ref, otherwise r. |
toRef(source, key?, default?) |
Create a ref that stays in sync with a property of a reactive object (or normalize a value/getter to a ref). |
toRefs(object) |
Convert all properties of a reactive object into refs, preserving reactivity (great for destructuring). |
toValue(source) |
Normalize a value, ref, or getter function to its plain value. |
proxyRefs(object) |
Return a proxy that auto-unwraps any refs accessed on it (used internally for setup returns). |
customRef(factory) |
Create a ref with custom dependency-tracking and trigger control (e.g. debounced refs). |
triggerRef(ref) |
Force effects depending on a shallowRef to run, after a deep mutation. |
import { ref, toRefs, reactive } from 'vue'
const state = reactive({ first: 'Ada', last: 'Lovelace' })
// Destructuring a reactive object loses reactivity — use toRefs:
const { first, last } = toRefs(state)
first.value = 'Grace' // still updates state.first
Tip
Use
customReffor patterns like a debounced search box. The factory receivestrackandtriggercallbacks so you control exactly when reads are recorded and when updates fire:import { customRef } from 'vue' function useDebouncedRef(value, delay = 200) { let timeout return customRef((track, trigger) => ({ get() { track(); return value }, set(newValue) { clearTimeout(timeout) timeout = setTimeout(() => { value = newValue; trigger() }, delay) }, })) }
4.2 Reactive objects
reactive makes an entire object (or array, Map, Set) deeply reactive using a JavaScript Proxy.
import { reactive } from 'vue'
const book = reactive({ title: 'Vue Internals', pages: 320 })
book.pages = 321 // triggers any effect that read book.pages
| Function | Purpose |
|---|---|
reactive(target) |
Deeply reactive proxy of an object. |
shallowReactive(target) |
Only the root-level properties are reactive. |
readonly(target) |
A deeply read-only proxy; writes are blocked (and warn in dev). |
shallowReadonly(target) |
Only root-level properties are read-only. |
isReactive(value) |
true for reactive/shallowReactive proxies. |
isReadonly(value) |
true for readonly/shallowReadonly proxies. |
isProxy(value) |
true if the value is any reactive or readonly proxy. |
isShallow(value) |
true for shallow reactive/readonly proxies. |
toRaw(proxy) |
Return the original, non-reactive object behind a proxy. |
markRaw(object) |
Mark an object so it is never converted to a proxy. |
import { reactive, readonly, toRaw, markRaw } from 'vue'
const original = { count: 0 }
const copy = reactive(original)
console.log(toRaw(copy) === original) // true
const config = markRaw({ thirdPartyInstance: new SomeBigClass() })
const state = reactive({ config }) // state.config stays raw
Warning
reactiveonly works on objects (including arrays and collection types). It cannot make a primitive value (string,number,boolean) reactive — usereffor those. Also note: destructuring areactiveobject disconnects the resulting variables from reactivity; usetoRefsto keep the link. Finally, replacing areactivevariable wholesale (state = reactive({...})) breaks reactivity because you discard the original proxy.
Note
reactivesilently skips objects that are not extensible (frozen or sealed) — callingreactiveon a frozen object returns it unchanged rather than wrapping it. This is reported behavior (issue #14893).
4.3 Computed properties
computed creates a reactive ref whose value is derived from other reactive sources and cached until a dependency changes.
import { ref, computed } from 'vue'
const count = ref(1)
const double = computed(() => count.value * 2)
console.log(double.value) // 2
count.value = 5
console.log(double.value) // 10
2
10
Pass an object with get and set to create a writable computed:
import { ref, computed } from 'vue'
const first = ref('Ada')
const last = ref('Lovelace')
const fullName = computed({
get: () => `${first.value} ${last.value}`,
set: (val) => {
[first.value, last.value] = val.split(' ')
},
})
fullName.value = 'Grace Hopper' // updates first and last
Tip
A computed is cached — its getter only re-runs when a reactive dependency it read actually changes. This makes computed the right tool for expensive derivations. For side effects (logging, fetching), use a watcher instead. In development, pass debug options to inspect dependencies:
const double = computed(() => count.value * 2, { onTrack(e) { /* called when a dep is tracked */ }, onTrigger(e) { /* called when a dep triggers */ }, })
4.4 Watchers
Watchers run a callback in response to reactive changes. Vue offers watch (explicit sources) and watchEffect (automatic dependency tracking).
| Function | Behavior |
|---|---|
watch(source, cb, options?) |
Run cb when source changes. Lazy by default. |
watchEffect(effect, options?) |
Run effect immediately, re-running whenever any reactive value it reads changes. |
watchPostEffect(effect) |
watchEffect with flush: 'post' (runs after DOM updates). |
watchSyncEffect(effect) |
watchEffect with flush: 'sync' (runs synchronously on each change). |
import { ref, watch, watchEffect } from 'vue'
const query = ref('')
const results = ref([])
// Explicit source, gets old and new values:
watch(query, async (newQuery, oldQuery) => {
results.value = await search(newQuery)
})
// Automatic tracking — re-runs whenever query.value changes:
watchEffect(() => {
console.log(`Searching for: ${query.value}`)
})
watch options:
| Option | Type | Purpose |
|---|---|---|
immediate |
boolean |
Run the callback once immediately on creation. |
deep |
boolean | number |
Deeply traverse objects so nested mutations trigger the callback. A number limits traversal depth. |
flush |
'pre' | 'post' | 'sync' |
When to run relative to component updates (default 'pre'). |
once |
boolean |
Run the callback at most once, then stop. |
onTrack / onTrigger |
function |
Dev-only debugging hooks. |
You can watch multiple sources by passing an array, and use getters for reactive-object properties:
import { reactive, watch } from 'vue'
const state = reactive({ a: 1, b: 2 })
watch(
() => [state.a, state.b],
([a, b], [prevA, prevB]) => {
console.log(`a: ${prevA} -> ${a}, b: ${prevB} -> ${b}`)
},
)
Cleaning up side effects. Inside a watcher you can register cleanup work that runs before the next invocation (or when the watcher stops) with onWatcherCleanup:
import { ref, watch, onWatcherCleanup } from 'vue'
const id = ref(1)
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/items/${newId}`, { signal: controller.signal })
onWatcherCleanup(() => controller.abort()) // cancel stale request
})
Stopping a watcher. watch and watchEffect return a stop handle. Calling it stops the watcher; it also exposes .pause() and .resume():
const stop = watchEffect(() => { /* ... */ })
stop.pause() // temporarily stop reacting
stop.resume() // resume
stop() // stop permanently
Warning
When you set
immediate: truetogether with an empty/never-changing array of sources, the callback runs once on creation but never again — there are no sources to react to. This is expected behavior and a common point of confusion (issue #14898). If you want a callback that re-runs on any dependency, usewatchEffect.
Important
By default
watchis shallow forreactivegetters that return the object itself — but when you pass areactiveobject directly as the source, Vue forcesdeep: true. To watch a specific nested value efficiently, pass a getter (() => state.some.nested) instead of the whole object.
4.5 Effect scopes
When you create watchers or computed values outside of a component (for example in a composable used at module scope), you can group their reactive effects into a scope so they can be disposed together.
import { effectScope, ref, watchEffect } from 'vue'
const scope = effectScope()
scope.run(() => {
const count = ref(0)
watchEffect(() => console.log(count.value))
})
// Later — dispose every effect created inside the scope at once:
scope.stop()
| API | Purpose |
|---|---|
effectScope(detached?) |
Create a scope. A detached scope is not collected by an outer scope. |
scope.run(fn) |
Run a function with this scope active; effects created inside are captured. |
scope.stop() |
Stop all effects captured by the scope. |
getCurrentScope() |
Return the currently active effect scope, if any. |
onScopeDispose(fn) |
Register a callback to run when the current scope is stopped. |
Tip
onScopeDisposeis the composition-friendly way to clean up in a composable without relying on a component lifecycle hook — it works anywhere there is an active scope (including inside a component'ssetup).
4.6 Low-level effects
For advanced use, effect and ReactiveEffect let you run a function that re-executes whenever its reactive dependencies change — this is the primitive that watchers and the renderer are built on.
import { reactive, effect, stop } from 'vue'
const state = reactive({ n: 0 })
const runner = effect(() => console.log(state.n))
// logs 0 immediately
state.n = 1 // logs 1
stop(runner) // stop re-running
Caution
effectis a low-level API. In application code preferwatchEffect, which integrates with the component lifecycle and is automatically stopped when the component unmounts. Reach foreffect/ReactiveEffect/stoponly when building reactivity infrastructure.
5. Components
5.1 Single-File Components (SFCs)
The idiomatic way to author Vue components is the .vue single-file component, which co-locates template, logic, and scoped styles:
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>
<template>
<button @click="count++">Count is: {{ count }}</button>
</template>
<style scoped>
button { font-weight: bold; }
</style>
<script setup> is compile-time sugar: every top-level binding (imports, variables, functions) is automatically exposed to the template. This is processed by @vue/compiler-sfc at build time.
5.2 The setup function and the Composition API
Without SFC sugar, a component defines a setup() function that returns the state and methods used by the template:
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const count = ref(0)
const increment = () => count.value++
return { count, increment }
},
})
defineComponent is a no-runtime-cost helper that gives you full TypeScript inference for component options. Use it whenever you define a component as a plain object (especially in TypeScript).
5.3 <script setup> compiler macros
Inside <script setup>, these compiler macros declare a component's interface. They are not imported at runtime — the SFC compiler transforms them — but Vue exports runtime stubs for typing and dev warnings.
| Macro | Purpose |
|---|---|
defineProps |
Declare the component's props. |
defineEmits |
Declare the events the component emits. |
defineExpose |
Explicitly expose properties to a parent via template refs. |
defineOptions |
Declare component options (e.g. name, inheritAttrs) inline. |
defineSlots |
Declare the slots the component accepts (typing only). |
defineModel |
Declare a two-way bindable model backed by a prop + update: event. |
withDefaults |
Provide default values for type-only defineProps. |
<script setup>
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
})
const emit = defineEmits(['submit', 'cancel'])
function save() {
emit('submit', props.title)
}
</script>
Two-way binding with defineModel:
<!-- CustomInput.vue -->
<script setup>
const model = defineModel() // backed by the `modelValue` prop + `update:modelValue`
</script>
<template>
<input v-model="model" />
</template>
<!-- Parent -->
<CustomInput v-model="searchText" />
5.4 Composition helpers
These functions are callable inside setup (or <script setup>) to access the component's runtime context:
| Function | Returns / Purpose |
|---|---|
useAttrs() |
The fallthrough attributes (everything not declared as a prop or emit). |
useSlots() |
The slots passed to this component. |
useModel(props, name, options?) |
The runtime helper behind defineModel. |
useTemplateRef(key) |
A ref bound to a template element/component with a matching ref="key". |
useId() |
A stable, SSR-safe unique ID (for for/aria-* attribute pairing). |
getCurrentInstance() |
The internal component instance (advanced/plugin use). |
hasInjectionContext() |
true if called where inject() would work. |
<script setup>
import { useTemplateRef, onMounted, useId } from 'vue'
const inputRef = useTemplateRef('the-input')
const id = useId()
onMounted(() => inputRef.value.focus())
</script>
<template>
<label :for="id">Name</label>
<input :id="id" ref="the-input" />
</template>
Tip
useIdgenerates IDs that match between the server-rendered HTML and the client, so it is the correct way to create unique IDs in apps that use SSR — hand-rolled counters cause hydration mismatches.
5.5 Lifecycle hooks
Register lifecycle callbacks inside setup. Each hook runs at a defined point in a component's life.
| Hook | Fires |
|---|---|
onBeforeMount |
Before the component's first render/DOM insertion. |
onMounted |
After the component is inserted into the DOM. |
onBeforeUpdate |
Before a re-render caused by reactive change. |
onUpdated |
After the DOM has been patched. |
onBeforeUnmount |
Before the component is removed. |
onUnmounted |
After the component and its effects are torn down. |
onActivated |
When a <KeepAlive>-cached component is re-inserted. |
onDeactivated |
When a <KeepAlive>-cached component is removed but kept alive. |
onErrorCaptured |
When an error from a descendant is captured (return false to stop propagation). |
onRenderTracked |
(dev) When a reactive dependency is tracked during render. |
onRenderTriggered |
(dev) When a dependency triggers a re-render. |
onServerPrefetch |
(SSR) Await async data before the component renders on the server. |
import { onMounted, onUnmounted } from 'vue'
export default {
setup() {
const onResize = () => { /* ... */ }
onMounted(() => window.addEventListener('resize', onResize))
onUnmounted(() => window.removeEventListener('resize', onResize))
},
}
Warning
Lifecycle hooks must be called synchronously during
setup. CallingonMountedinside anawait, asetTimeout, or a.then()callback registers it against the wrong (or no) component instance and it will not fire.
5.6 Provide / Inject
Pass data to deeply nested descendants without prop-drilling.
// Ancestor
import { provide, ref } from 'vue'
const theme = ref('dark')
provide('theme', theme)
// Any descendant
import { inject } from 'vue'
const theme = inject('theme', 'light') // second arg is the default
| Function | Purpose |
|---|---|
provide(key, value) |
Provide a value to descendants. key can be a string or an InjectionKey<T> symbol for typing. |
inject(key, default?, treatDefaultAsFactory?) |
Read a provided value, with an optional default. |
hasInjectionContext() |
Check whether injection is currently possible. |
Tip
For type-safe injection in TypeScript, create a typed key with
InjectionKey:import { type InjectionKey, provide, inject } from 'vue' const ThemeKey: InjectionKey<Ref<string>> = Symbol('theme') provide(ThemeKey, theme) // value is type-checked const theme = inject(ThemeKey) // inferred as Ref<string> | undefined
5.7 Async components
Load a component lazily — its code is only fetched when it is first rendered.
import { defineAsyncComponent } from 'vue'
const AsyncDashboard = defineAsyncComponent(() =>
import('./Dashboard.vue'),
)
Use the options form for loading/error states and timeouts:
const AsyncDashboard = defineAsyncComponent({
loader: () => import('./Dashboard.vue'),
loadingComponent: LoadingSpinner,
errorComponent: LoadError,
delay: 200, // ms before showing the loading component
timeout: 10000, // ms before failing with the error component
})
Warning
If the loader resolves to something that is not a valid component, Vue throws
Invalid async component load result. Make sure dynamicimport()returns a module whose default export is a component (or return the component directly).
6. Template Syntax & Directives
Vue templates are HTML augmented with directives (v- prefixed attributes), interpolation ({{ }}), and bindings.
6.1 Text interpolation and binding
<template>
<!-- text interpolation -->
<span>{{ message }}</span>
<!-- attribute binding (shorthand `:`) -->
<img :src="imageUrl" :alt="caption" />
<!-- event binding (shorthand `@`) -->
<button @click="onClick">Go</button>
<!-- two-way binding on form inputs -->
<input v-model="name" />
</template>
6.2 Built-in directives
| Directive | Purpose |
|---|---|
v-bind (:) |
Bind an attribute or prop to an expression. |
v-on (@) |
Attach an event listener. |
v-model |
Two-way binding on form elements and components. |
v-if / v-else-if / v-else |
Conditionally render an element (removed from DOM when false). |
v-show |
Toggle an element's CSS display (stays in DOM). |
v-for |
Render a list from an array, object, number, or iterable. |
v-slot (#) |
Define/consume named and scoped slots. |
v-html |
Set an element's innerHTML. |
v-text |
Set an element's text content. |
v-pre |
Skip compilation for this element and its children. |
v-once |
Render once and never update. |
v-memo |
Memoize a subtree, re-rendering only when listed dependencies change. |
v-cloak |
Hide un-compiled templates until Vue is ready (full build). |
Caution
v-htmlrenders raw HTML and can expose your app to cross-site scripting (XSS). Never usev-htmlon content that comes from users or any untrusted source. Prefer text interpolation, which is always escaped.
6.3 v-model modifiers and form binding
v-model works on <input>, <textarea>, <select>, and components. Built-in modifiers transform the bound value:
| Modifier | Effect |
|---|---|
.lazy |
Sync on change instead of input. |
.number |
Cast the input string to a number. |
.trim |
Trim whitespace from the input. |
<input v-model.trim.number="age" />
6.4 Event modifiers
v-on/@ supports modifiers for common event handling needs:
<form @submit.prevent="onSubmit"> <!-- event.preventDefault() -->
<input @keyup.enter="search" /> <!-- only the Enter key -->
<button @click.stop="onClick">Go</button> <!-- event.stopPropagation() -->
</form>
For programmatic handlers (render functions / JSX), Vue exports withModifiers and withKeys:
import { withModifiers, withKeys, h } from 'vue'
h('input', {
onKeyup: withKeys(() => submit(), ['enter']),
onClick: withModifiers(() => go(), ['stop', 'prevent']),
})
6.5 List rendering with v-for
<ul>
<li v-for="(item, index) in items" :key="item.id">
{{ index }}: {{ item.name }}
</li>
</ul>
Important
Always provide a
:keywithv-forso Vue can track identity across updates. Without keys, Vue falls back to in-place patching, which can produce incorrect results when list items have their own state.
Note
When iterating an object with
v-for="(value, key) in obj", thekeyis typed asstring(object keys are strings in JS). If you iterate with key/value and expect a numeric key, you must convert it yourself (related: issue #14910).
7. Built-in Components
Vue ships several components you can use directly in any template. The runtime-core components are available everywhere; Transition and TransitionGroup are DOM-specific (from @vue/runtime-dom, re-exported by vue).
| Component | Purpose |
|---|---|
<Teleport> |
Render child content to a different place in the DOM (e.g. modals at <body>). |
<Suspense> |
Coordinate nested async dependencies, showing fallback content while pending. |
<KeepAlive> |
Cache toggled components so they keep state instead of being destroyed. |
<Transition> |
Apply enter/leave animations to a single element or component. |
<TransitionGroup> |
Animate insertion, removal, and reordering of list items. |
7.1 Teleport
<template>
<button @click="open = true">Open modal</button>
<Teleport to="body">
<div v-if="open" class="modal">
Modal content rendered at <body>
<button @click="open = false">Close</button>
</div>
</Teleport>
</template>
<Teleport> props: to (target selector or element), disabled (render in place when true), and defer (resolve the target after the current render, for targets rendered by Vue itself).
7.2 Suspense
<template>
<Suspense>
<template #default>
<AsyncProfile /> <!-- a component with async setup() -->
</template>
<template #fallback>
<div>Loading profile…</div>
</template>
</Suspense>
</template>
Note
<Suspense>is an experimental feature; its API may evolve. It works with components that use top-levelawaitinsetuporonServerPrefetch.
7.3 KeepAlive
<KeepAlive :include="['Home', 'Profile']" :max="10">
<component :is="currentView" />
</KeepAlive>
<KeepAlive> props: include / exclude (component name matchers — string, RegExp, or array) and max (cap the number of cached instances, LRU-evicted). Cached components fire onActivated/onDeactivated instead of mount/unmount.
7.4 Transition
<template>
<Transition name="fade">
<p v-if="show">Hello</p>
</Transition>
</template>
<style>
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
</style>
Key <Transition> props include name, mode ('in-out' / 'out-in'), appear, duration, and per-phase class overrides (enter-active-class, etc.). <TransitionGroup> additionally renders a real wrapper element (via tag) and applies a move-class for reorder animations.
Warning
<Transition>animates an element's presence; it does not animatevisibility. Togglingvisibilityviav-showwill not produce a transition the waydisplay/opacitydo (related: issue #2531). Animateopacity/transforminstead.
8. Server-Side Rendering (SSR)
Vue can render components to HTML strings or streams on the server using the vue/server-renderer entry (the @vue/server-renderer package).
import { createSSRApp } from 'vue'
import { renderToString } from 'vue/server-renderer'
const app = createSSRApp({
data: () => ({ msg: 'Hello from the server' }),
template: `<div>{{ msg }}</div>`,
})
const html = await renderToString(app)
console.log(html) // <div>Hello from the server</div>
| Function | Output |
|---|---|
renderToString(app, context?) |
Render to a complete HTML string (Promise). |
renderToNodeStream(app, context?) |
Render to a Node.js Readable stream. |
pipeToNodeWritable(app, context, writable) |
Pipe rendering into a Node.js Writable. |
renderToWebStream(app, context?) |
Render to a Web ReadableStream. |
pipeToWebWritable(app, context, writable) |
Pipe rendering into a Web WritableStream. |
renderToSimpleStream(app, context, push) |
Render with a minimal push-based stream interface. |
On the client, you "hydrate" the server-rendered markup instead of re-rendering it. Use createSSRApp (not createApp) so Vue attaches to existing DOM:
import { createSSRApp } from 'vue'
import App from './App.vue'
createSSRApp(App).mount('#app') // hydrates the server HTML in place
Important
The client app structure must match the server-rendered output exactly, or you get a hydration mismatch. Use
useId()for any generated IDs and avoid rendering values that differ between server and client (likeDate.now()or random numbers) without guarding them.
Tip
renderToStreamis deprecated in favor ofrenderToNodeStream/renderToWebStream. Prefer the explicit stream variants for new code.
9. Custom Elements (Web Components)
Vue can compile any component into a standards-based custom element you can use outside Vue — in plain HTML, or in another framework.
import { defineCustomElement } from 'vue'
import MyWidget from './MyWidget.ce.vue'
const MyWidgetElement = defineCustomElement(MyWidget)
customElements.define('my-widget', MyWidgetElement)
<my-widget some-prop="value"></my-widget>
| API | Purpose |
|---|---|
defineCustomElement(component, options?) |
Build a custom-element constructor from a Vue component. |
defineSSRCustomElement(component, options?) |
Same, for server-rendered custom elements. |
useHost() |
Access the host custom element from inside the component. |
useShadowRoot() |
Access the component's shadow root. |
VueElement |
The base class custom elements extend. |
Note
A
.ce.vuesuffix (orcustomElement: truein your build tool) tells the SFC compiler to inject component styles into the shadow root instead of the document head, which is the correct behavior for custom elements.
10. Render Functions & JSX
When templates are not expressive enough, write render functions directly with h (hyperscript).
import { h, defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const count = ref(0)
return () =>
h('button', { onClick: () => count.value++ }, `Count: ${count.value}`)
},
})
| API | Purpose |
|---|---|
h(type, props?, children?) |
Create a vnode (the main render-function API). |
mergeProps(...objects) |
Merge multiple props objects (class/style/handlers merged correctly). |
cloneVNode(vnode, extraProps?) |
Clone a vnode with extra props. |
isVNode(value) |
Check if a value is a vnode. |
resolveComponent(name) |
Resolve a registered component by name in a render function. |
resolveDirective(name) |
Resolve a registered directive by name. |
withDirectives(vnode, directives) |
Attach custom directives to a vnode. |
Fragment, Text, Comment, Static |
Special vnode type markers. |
JSX is supported via the vue/jsx-runtime automatic runtime — configure your build's jsxImportSource to vue and write JSX that compiles to h calls.
11. Utility & Timing APIs
| API | Purpose |
|---|---|
nextTick(callback?) |
Wait for the next DOM update flush. Returns a Promise. |
version |
The Vue version string (import { version } from 'vue'). |
toDisplayString(value) |
The interpolation stringifier Vue uses for {{ }}. |
normalizeClass(value) |
Normalize a class binding (string/array/object) to a class string. |
normalizeStyle(value) |
Normalize a style binding to a style object/string. |
useCssModule(name?) |
Access CSS Modules classes inside setup. |
useCssVars(getter) |
Drive <style> CSS custom properties from reactive state. |
import { ref, nextTick } from 'vue'
const count = ref(0)
count.value++
await nextTick()
// the DOM now reflects the new count
Tip
Use
nextTickwhen you need to read the DOM after a reactive change has been applied — for example, measuring an element's size after its content changed.
12. How Vue Works (Concepts)
You can be productive with Vue without this section, but understanding the pipeline helps you debug and optimize.
- Reactivity —
ref/reactivewrap your state in proxies. When code reads a reactive value during an effect (like a component's render), Vue records a dependency. When the value changes, Vue re-runs the dependent effects. - Template compilation — Your
<template>(or string template) is compiled into a render function that returns a tree of vnodes (virtual DOM nodes). With SFCs and a build step, this happens ahead of time; with the full build, it happens in the browser. - Rendering & patching — On first render, Vue creates real DOM from the vnode tree. On updates, it produces a new vnode tree and patches only the differences into the real DOM.
- Compiler-informed fast paths — Vue's compiler annotates the output with hints (patch flags, static hoisting, block tracking) so the runtime skips work for parts of the tree that cannot change. This is why Vue templates are typically faster than hand-written render functions.
The packages map onto these stages: @vue/reactivity (state), @vue/compiler-dom + @vue/compiler-sfc (compilation), @vue/runtime-core + @vue/runtime-dom (rendering), and @vue/server-renderer (SSR). The umbrella vue package wires them together.
13. Troubleshooting
Error: Failed to mount app: mount target selector "#app" returned null.
Cause: app.mount('#app') was called with a CSS selector that matched no element — usually because the script ran before the target existed in the DOM, or the selector is wrong.
Solution: Ensure the target element exists before mounting (place the script at the end of <body> or load it as a module/deferred), and verify the selector matches your HTML's id.
Prevention: Use a module bundler entry that runs after DOM parse, or wrap mounting in a DOMContentLoaded listener for plain-script setups.
Error: Template element not found or is empty: #template
Cause: The full build was told to use an in-DOM template via a selector (template: '#template'), but no element with that id was found.
Solution: Confirm the referenced template element exists in the page and contains markup. For build-step apps, use SFCs instead of selector templates.
Error: invalid template option:
Cause: A template option was provided that is neither a string nor a DOM node.
Solution: Pass a string template or a real DOM element. If you meant to render a component, supply it as the root component to createApp, not as template.
Error: Component provided template option but runtime compilation is not supported
Cause: You are using the runtime-only build (the default vue import) but passed a string template that needs the compiler.
Solution: Either (a) use Single-File Components compiled by your build tool (recommended), or (b) switch to the full build by aliasing vue to vue/dist/vue.esm-bundler.js in your bundler config.
Prevention: Keep templates in .vue files so they compile at build time.
Warning: Template compilation error: Element is missing end tag
Cause: A template (compiled by the full build) contains malformed HTML, e.g. <div v-if> with no closing tag, or a self-closing native element that breaks parsing.
Solution: Fix the markup — close all elements. Vue prints a code frame pointing at the offending line.
Warning: Set operation on key "…" failed: target is readonly.
Cause: Code attempted to mutate a readonly/shallowReadonly proxy — commonly a prop (props are readonly) or an injected readonly value.
Solution: Do not mutate props directly. Emit an event to ask the parent to change the value, or use defineModel/v-model for two-way binding. For injected state, provide a setter function alongside the value.
Warning: Reactive Map/Set contains both the raw and reactive versions of the same object
Cause: The same object was used as a key/value in a reactive Map/Set in both its raw and reactive forms, which leads to inconsistent lookups.
Solution: Standardize on one form. Convert with toRaw() (or always use the reactive proxy) before inserting into a reactive collection.
Problem: Destructured reactive state stopped updating
Cause: Destructuring a reactive object copies primitive values, severing the reactive connection.
Solution: Wrap with toRefs(state) before destructuring, then use .value (or rely on template unwrapping).
Problem: Replacing a ref object or reactive variable breaks reactivity
Cause: Reassigning a whole reactive variable (state = reactive({...})) discards the tracked proxy. For arrays, replacing the array reference inside a ref is fine (list.value = [...]), but reassigning a reactive array variable is not.
Solution: Mutate the existing reactive object/array in place, or store the value in a ref and replace ref.value.
Problem: onMounted (or another hook) never fires
Cause: The hook was registered asynchronously (inside await, setTimeout, or a promise callback), so it was not associated with the active component instance.
Solution: Register all lifecycle hooks synchronously at the top level of setup.
Problem: Hydration mismatch warnings after SSR
Cause: The client-rendered tree differs from the server HTML — often from non-deterministic values (dates, random IDs) or invalid HTML nesting.
Solution: Use useId() for generated IDs, guard environment-specific rendering, and ensure valid HTML nesting. Enable __VUE_PROD_HYDRATION_MISMATCH_DETAILS__ to get specific diagnostics in production.
Problem: watch callback with immediate: true only runs once
Cause: With an empty or never-changing source, there is nothing to react to after the initial immediate run.
Solution: Watch a real reactive source, or use watchEffect if you want the callback to track whatever it reads.
14. FAQ
Q: Should I use ref or reactive?
Use ref for primitives and when you want a single, reassignable container (x.value = ...). Use reactive for objects you mutate in place. Many teams standardize on ref everywhere for consistency, since ref can hold objects too.
Q: What's the difference between watch and watchEffect?
watch takes explicit sources and gives you old + new values, and is lazy by default. watchEffect runs immediately and automatically re-runs when any reactive value it reads changes, but doesn't give you previous values.
Q: When do I need nextTick?
Whenever you must read or act on the DOM after a reactive state change has been flushed to it — Vue batches DOM updates, so they aren't applied synchronously when you change state.
Q: Do I have to use Single-File Components?
No. You can author components as plain objects with setup()/defineComponent, or with render functions/JSX. SFCs are the most ergonomic for most apps because they co-locate template, logic, and scoped CSS.
Q: How do I do two-way binding on a custom component?
Use defineModel() in the child (<script setup>) and v-model in the parent. Under the hood this is a modelValue prop plus an update:modelValue event.
Q: Which build do I get by default from import ... from 'vue'?
The runtime-only ESM bundler build (vue.runtime.esm-bundler.js). It excludes the template compiler because your SFC templates are compiled at build time.
Q: How do I enable the Options API tree-shaking?
Set the bundler flag __VUE_OPTIONS_API__ to false. Only do this if no component (or dependency) relies on the Options API.
Q: Can I use Vue without a build step?
Yes — load vue.global.js (or vue.esm-browser.js) from a CDN. These are full builds that include the compiler, so in-page templates work.
Q: How do I share logic between components?
Write a composable: a function that uses the Composition API (ref, computed, watch, lifecycle hooks) and returns reactive state. Call it from any component's setup.
Q: How do I clean up side effects in a composable?
Use onScopeDispose (works with any active effect scope) or a lifecycle hook like onUnmounted when inside a component. For per-run cleanup in a watcher, use onWatcherCleanup.
Q: Is <Suspense> production-ready?
It is still marked experimental. It works today for async setup/onServerPrefetch, but treat its API as subject to change.
Q: What Node version do I need?
Vue's tooling requires Node.js >= 20. The runtime itself runs in modern browsers (ES2016+) and in Node for SSR.
15. API Quick Reference
Reactivity (vue / @vue/reactivity): ref · shallowRef · isRef · unref · toRef · toRefs · toValue · proxyRefs · customRef · triggerRef · reactive · shallowReactive · readonly · shallowReadonly · isReactive · isReadonly · isProxy · isShallow · toRaw · markRaw · computed · watch · watchEffect · watchPostEffect · watchSyncEffect · onWatcherCleanup · effect · stop · effectScope · getCurrentScope · onScopeDispose
Component & lifecycle (vue): defineComponent · defineAsyncComponent · onBeforeMount · onMounted · onBeforeUpdate · onUpdated · onBeforeUnmount · onUnmounted · onActivated · onDeactivated · onErrorCaptured · onRenderTracked · onRenderTriggered · onServerPrefetch · provide · inject · hasInjectionContext
<script setup> macros: defineProps · defineEmits · defineExpose · defineOptions · defineSlots · defineModel · withDefaults
Composition helpers: useAttrs · useSlots · useModel · useTemplateRef · useId · useCssModule · useCssVars · getCurrentInstance
App & rendering: createApp · createSSRApp · nextTick · h · mergeProps · cloneVNode · isVNode · resolveComponent · resolveDirective · withDirectives · withModifiers · withKeys · version
Built-in components: Teleport · Suspense · KeepAlive · Transition · TransitionGroup
Custom elements: defineCustomElement · defineSSRCustomElement · useHost · useShadowRoot · VueElement
SSR (vue/server-renderer): renderToString · renderToNodeStream · pipeToNodeWritable · renderToWebStream · pipeToWebWritable · renderToSimpleStream