KeepAlive, Teleport, and Suspense

This section documents the current Vue 3.x release line as published at the official Vue.js documentation, which is the reference these pages are written and verified against. No specific patch version is pinned. The Composition API with <script setup> is the authoring style used throughout; the Options API is shown only as a contrast.

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production, since Vue and its ecosystem iterate quickly.

This section’s bibliography lists the reference material consulted while preparing these pages.

<KeepAlive>, <Teleport>, and <Suspense> are built-in components that change where and how a subtree lives: kept in memory instead of destroyed, rendered into a different part of the DOM, or held back behind a fallback until its async dependencies resolve.

<KeepAlive>

A component toggled out by v-if or replaced through :is is normally unmounted, losing its state. Wrap it in <KeepAlive> to cache the instance instead:

<script setup>
import { shallowRef } from 'vue'
import Home from './Home.vue'
import Settings from './Settings.vue'

const tabs = { Home, Settings }
const current = shallowRef('Home')
</script>

<template>
  <button v-for="(_, name) in tabs" :key="name" @click="current = name">
    {{ name }}
  </button>
  <KeepAlive>
    <component :is="tabs[current]" />
  </KeepAlive>
</template>

Switching tabs now preserves scroll position, form input, and any other in-component state. See <KeepAlive>.

include, exclude, max

By default every child is cached. Restrict the set by component name (a string, comma-separated string, RegExp, or array of those), and bound the pool with max, which turns it into an LRU cache:

<KeepAlive :include="['Home', 'Settings']" :exclude="/Modal$/" :max="10">
  <component :is="view" />
</KeepAlive>

Matching is against each component’s name option, or the name inferred from its filename in <script setup> (Vue 3.3+).

onActivated / onDeactivated

A cached component is not unmounted, so onMounted and onUnmounted do not fire when it is toggled. Use the KeepAlive-specific hooks instead:

<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // re-entered from the cache -- resume polling, refetch, restore focus
})
onDeactivated(() => {
  // cached away -- pause timers and subscriptions
})
</script>

onActivated also runs on the initial mount and onDeactivated on the final unmount. Both hooks fire for the component and for any descendant that is also inside the cached tree. See onActivated.

<Teleport>

Renders its slot content at a different location in the DOM while keeping it a logical child of the current component — props, provide/inject, and emitted events all still work. The classic case is a modal that must escape an ancestor’s overflow: hidden or z-index stacking context:

<script setup>
import { ref } from 'vue'
const open = ref(false)
</script>

<template>
  <button @click="open = true">open</button>
  <Teleport to="body">
    <div v-if="open" class="backdrop" @click="open = false">
      <div class="modal">I render at the end of &lt;body&gt;</div>
    </div>
  </Teleport>
</template>

to accepts a CSS selector string or an actual DOM element. See <Teleport>.

disabled

Toggle teleporting at runtime — for example, keep a video player inline on desktop but move it to a full-screen container on mobile:

<Teleport to="#fullscreen" :disabled="!isMobile">
  <VideoPlayer :src="src" />
</Teleport>

Deferred teleport

If the target element is rendered by Vue later in the same tree, it does not exist yet when <Teleport> mounts. Since Vue 3.5, defer postpones resolving the target until after the current render tick:

<template>
  <Teleport defer to="#late-panel">
    <p>waits one tick for the target to exist</p>
  </Teleport>
  <!-- rendered by Vue, after the Teleport above -->
  <div id="late-panel"></div>
</template>

Multiple teleports to one target

Several <Teleport> components aimed at the same target append their content in mount order — this is how a toast or notification stack is built:

<template>
  <Teleport to="#toasts"><Toast>Saved</Toast></Teleport>
  <Teleport to="#toasts"><Toast>Uploaded</Toast></Teleport>
</template>

<Suspense>

<Suspense> is an experimental feature — its API may still change.

<Suspense> shows a fallback until every async dependency in its #default slot has resolved. Async dependencies are async components and components whose <script setup> contains a top-level await:

<template>
  <Suspense>
    <template #default>
      <UserDashboard />
    </template>
    <template #fallback>
      <p>Loading dashboard…</p>
    </template>
  </Suspense>
</template>
<script setup>
// UserDashboard.vue -- top-level await makes this an async dependency
const res = await fetch('/api/me')
const user = await res.json()
</script>

<template>
  <h1>{{ user.name }}</h1>
</template>

See <Suspense>.

Events and state flow

<Suspense> emits an event at each stage:

stateDiagram-v2 [*] --> pending: mount or keyed re-render pending --> fallback: deps still unresolved (or timeout on update) pending --> resolve: all default-slot deps resolved fallback --> resolve: deps resolved resolve --> [*]: default slot displayed
Event Fires when

@pending

a new async branch begins — initial load, or a keyed re-render

@resolve

every dependency in #default has resolved and it is about to be shown

@fallback

the fallback slot is displayed — on initial load, or when timeout elapses on an update

<Suspense> also accepts timeout (milliseconds to wait before showing the fallback on an update) and suspensible (defer handling to a parent <Suspense>). Pair it with a defineAsyncComponent boundary, and handle the rejected case with an error boundary using onErrorCaptured.

Special elements and attributes

These read like components or plain attributes but are handled by the compiler. They are listed in Built-in Special Elements and Special Attributes.

Name Purpose

<component :is>

render a component chosen at runtime — a registered name, an imported component object, or a plain HTML tag string. Combine with <KeepAlive> and <Transition> above.

<slot>

outlet for parent-supplied content; <slot name="header" :row="row" /> also passes scoped-slot props back to the parent.

<template>

a wrapper that renders nothing — used to host v-if, v-for, or v-slot, and to return multiple root nodes.

key

a hint to replace rather than patch a vnode — essential on <component :is> swaps and on v-for items.

ref

obtain an element or component instance — a string name in the template, read via useTemplateRef('name') in <script setup> (Vue 3.5+).

is

on a native tag, <tr is="vue:my-row"> works around HTML parsing restrictions inside <table>, <select>, and similar.

<script setup>
import { useTemplateRef, onMounted } from 'vue'
const field = useTemplateRef('field')
onMounted(() => field.value.focus())
</script>

<template>
  <input ref="field" />
</template>

See also

  • Async Components — defineAsyncComponent, loading and error states, and how it pairs with <Suspense>.

  • Transitions and Animation — animating <component :is> swaps and teleported modals.

  • Routing — <RouterView> with <KeepAlive> and <Transition> for cached, animated route views.