| |

Vue.js 45 🟢 Deferred Teleport Execution using the defer Prop

A <Teleport> normally resolves its target the moment it mounts. If the target element does not exist yet, Vue logs a warning and renders nothing. This is a common problem when the target is rendered by the same component tree: a modal might teleport to a container that appears later in the template, or a layout might render the portal root after the component that needs it. Vue 3.5 introduced the defer prop to solve exactly this. It tells the teleport to postpone resolving its target until after the rest of the application has mounted in the same tick, so the target is guaranteed to exist when the teleport finally looks for it .

Key point: defer postpones the target lookup to the end of the current mount/update cycle. It does not wait for asynchronous rendering, Suspense boundaries, or a later tick. The target must still be rendered by the same component tree in the same cycle.


Why the defer prop exists

The timing problem. Teleport resolves its target during the patch phase, before any lifecycle hooks run . If the target is rendered later in the template, the patch that would create it has not executed yet. The teleport looks for an element that does not exist and fails.

The SSR problem. Server-side rendering mounts the tree synchronously. A teleport that targets an element rendered later in the same tree fails on the server, which breaks hydration .

The workaround problem. Before defer, developers worked around the timing issue with a disabled binding and a nextTick call: render the content inline, then enable the teleport once the target exists . This works but introduces a second update cycle and the race conditions that come with it.

The layout problem. A modal that needs to be positioned relative to its parent container cannot teleport to body without losing that relationship. The target has to be an element rendered nearby in the tree, and the defer prop makes that possible .

The same-tick guarantee. defer only works when the target is rendered in the same mount or update cycle. It is a scheduling fix, not a general-purpose waiting mechanism. If the target is inside a Suspense boundary that resolves asynchronously, defer will not help .


a. Using the defer prop

The defer prop is a boolean on the <Teleport> component. It requires Vue 3.5 or later .

The basic pattern

<template>
  <Teleport defer to="#late-container">
    <div class="modal">Deferred content</div>
  </Teleport>

  <!-- Target rendered later in the same template -->
  <div id="late-container"></div>
</template>

Without defer, the teleport looks for #late-container during the patch phase. The element does not exist yet, so the teleport fails. With defer, Vue queues a post-render effect. After the entire tree has mounted, the effect resolves the target and moves the content .

The behavioral change

The content is not rendered inline while waiting. It is held until the target is resolved. If the target never appears in the same cycle, the content is not rendered at all and Vue logs a warning .

The lifecycle timing

With defer, the teleported content’s onMounted hook fires later than the parent’s onMounted. The parent mounts first, then the deferred teleport resolves its target, then the teleported content mounts. If the content depends on a parent ref being ready, the ref is available by the time the content mounts .

The implementation detail

Internally, Vue tracks a __isMounted flag on the teleport’s element. The deferred processing runs only if the teleport is deferred and has not yet mounted. This prevents the deferred mount from firing after the component has been unmounted .

The limitations

defer does not help when the target is inside a <Suspense> boundary that resolves asynchronously. It also does not help when the target is rendered by a dynamically imported component that loads later. The target must be in the same synchronous render pass .


b. When to use defer

The defer prop is a targeted solution for a specific timing problem. It is not a general replacement for body as a teleport target.

The same-tree target

The primary use case is a target rendered by the same component or a sibling component in the same tree. A layout component might render a portal container at the end of its template, and a child might need to teleport into it.

<!-- Layout.vue -->
<template>
  <div class="layout">
    <slot />
    <div id="modal-root"></div>
  </div>
</template>
<!-- Page.vue (child of Layout) -->
<template>
  <Teleport defer to="#modal-root">
    <Modal v-if="open" />
  </Teleport>
</template>

Without defer, the teleport fails because #modal-root is rendered after the slot content. With defer, the target resolves after the layout mounts .

The SSR consideration

In server-rendered applications, the target must exist in the server-rendered HTML. If the target is rendered by a Vue component, defer ensures it is in the tree by the time the teleport resolves. But the target must still be rendered on the server. A target created only on the client will not hydrate correctly .

The responsive fallback

The defer prop composes with disabled. A teleport can be deferred on desktop and disabled on mobile, so the content renders inline when the target is not wanted.

<Teleport defer to="#modal-root" :disabled="isMobile">
  <Modal />
</Teleport>

The preference for stable targets

The Vue documentation still recommends body or a pre-existing element in index.html as the ideal target for global overlays. defer is the answer when the target must be rendered by Vue, not a replacement for the stable-container pattern .

The decision table

ScenarioRecommended
Target in index.htmlNo defer needed
Target rendered by the same treedefer
Target inside SuspenseNot supported by defer
Target on mobile should be inlinedefer + :disabled
Target must persist across routesbody or index.html

c. Deferred teleport versus alternatives

Before defer, the timing problem was solved with workarounds. The alternatives still apply in cases where defer does not fit.

The disabled plus nextTick workaround

The classic workaround renders the teleport inline first, then enables it after the target exists.

<script setup>
import { ref, onMounted } from 'vue';
const ready = ref(false);
onMounted(() => { ready.value = true; });
</script>

<template>
  <Teleport to="#modal-root" :disabled="!ready">
    <Modal />
  </Teleport>
  <div id="modal-root"></div>
</template>

The teleport renders inline until ready is true, then moves the content. This works but causes a second update cycle and can produce a visible flicker. defer avoids this by scheduling the target resolution once, in the same cycle .

The provide-inject target pattern

A more complex workaround uses a shared reactive ref to track whether the target has mounted. Components register their target element, and the teleport reads the ref.

// useTeleportTarget.ts
const teleportMap: Record<string, Ref<HTMLElement | undefined>> = {};

export function useTeleportTarget(elementId: string) {
  if (!teleportMap[elementId]) {
    teleportMap[elementId] = ref<HTMLElement>();
  }
  return teleportMap[elementId];
}

This is the pattern used by some UI libraries before defer existed . It is more flexible but requires coordination across components. defer handles the same case with a single prop.

The body target

The simplest alternative is to teleport to body and accept the layout change. This is stable and requires no timing coordination, but it loses the positional relationship with the parent. A modal that needs to be centered relative to a specific container cannot use this approach .

The comparison

ApproachTimingFlickerLayout preservedComplexity
deferSame cycleNoYesLow
:disabled + nextTickNext cyclePossibleYesMedium
Provide/inject targetAnyNoYesHigh
body targetImmediateNoNoLow

Complete Example Session

<!-- ============================================
     PART 1: THE TIMING PROBLEM
     ============================================ -->

<!-- Without defer, this fails. -->
<template>
  <Teleport to="#late-container">
    <Modal />
  </Teleport>
  <div id="late-container"></div>
</template>

<!-- Vue warns: Invalid Teleport target on mount: null -->

<!-- ============================================
     PART 2: THE DEFER SOLUTION
     ============================================ -->

<template>
  <Teleport defer to="#late-container">
    <Modal />
  </Teleport>
  <div id="late-container"></div>
</template>

<!-- The target resolves after the tree mounts. -->

<!-- ============================================
     PART 3: LAYOUT COMPONENT WITH PORTAL ROOT
     ============================================ -->

<!-- Layout.vue -->
<template>
  <div class="layout">
    <header>...</header>
    <slot />
    <div id="modal-root"></div>
  </div>
</template>

<!-- Page.vue -->
<template>
  <Teleport defer to="#modal-root">
    <Modal v-if="open" @close="open = false" />
  </Teleport>
  <button @click="open = true">Open</button>
</template>

<!-- ============================================
     PART 4: LIFECYCLE ORDER
     ============================================ -->

<!-- Parent.vue -->
<script setup>
onMounted(() => console.log('parent mounted'));
</script>

<template>
  <Teleport defer to="#target">
    <Child />  <!-- onMounted fires after parent -->
  </Teleport>
  <div id="target"></div>
</template>

<!-- Output: parent mounted, then child mounted -->

<!-- ============================================
     PART 5: DEFER WITH DISABLED
     ============================================ -->

<script setup>
import { useMediaQuery } from '@vueuse/core';
const isMobile = useMediaQuery('(max-width: 768px)');
</script>

<template>
  <Teleport defer to="#modal-root" :disabled="isMobile">
    <Modal />
  </Teleport>
  <div id="modal-root"></div>
</template>

<!-- On mobile: inline. On desktop: deferred teleport. -->

<!-- ============================================
     PART 6: THE WORKAROUND COMPARISON
     ============================================ -->

<!-- Old workaround: disabled + nextTick -->
<script setup>
import { ref, onMounted } from 'vue';
const ready = ref(false);
onMounted(() => { ready.value = true; });
</script>

<template>
  <Teleport to="#target" :disabled="!ready">
    <Modal />
  </Teleport>
  <div id="target"></div>
</template>

<!-- New: defer -->
<template>
  <Teleport defer to="#target">
    <Modal />
  </Teleport>
  <div id="target"></div>
</template>

<!-- ============================================
     PART 7: WHAT DEFER DOES NOT SOLVE
     ============================================ -->

<!-- Target inside Suspense: defer does not help. -->
<template>
  <Teleport defer to="#async-target">
    <Modal />
  </Teleport>
  <Suspense>
    <AsyncComponent />  <!-- Contains #async-target -->
  </Suspense>
</template>
<!-- The target mounts asynchronously. defer waits only for the same tick. -->

<!-- ============================================
     PART 8: SSR CONSIDERATION
     ============================================ -->

<!-- Server-rendered HTML must contain the target. -->
<body>
  <div id="app">
    <!-- Layout renders #modal-root -->
  </div>
</body>

<!-- The client hydrates with defer. The target exists. -->

<!-- ============================================
     PART 9: PREFERRED PATTERN
     ============================================ -->

<!-- Best: target in index.html. -->
<body>
  <div id="app"></div>
  <div id="modal-root"></div>
</body>

<!-- The teleport needs no defer. -->
<Teleport to="#modal-root">
  <Modal />
</Teleport>

<!-- ============================================
     PART 10: DECISION GUIDE
     ============================================ -->

<!--
Target in index.html?
  → No defer. Use to="#modal-root".

Target rendered by Vue in the same tree?
  → Use defer.

Target inside Suspense or async component?
  → defer does not work. Render the target earlier or use a different approach.

Target should be inline on mobile?
  → defer + :disabled.
-->

The ten parts showed the timing problem, the defer solution, the layout component pattern, lifecycle order changes, defer with disabled, the workaround comparison, what defer does not solve, SSR considerations, the preferred stable-target pattern, and the decision guide.


Quick Reference

The defer Prop

AspectValue
TypeBoolean
Vue version3.5+
EffectPostpones target resolution to the end of the mount/update cycle
Target requirementMust render in the same cycle
LifecycleContent mounts after parent
SSRTarget must exist in server-rendered HTML

When to Use defer

Scenariodefer?
Target in index.htmlNo
Target rendered by same treeYes
Target inside SuspenseNo (does not work)
Target rendered asynchronouslyNo (does not work)
Mobile should render inlineYes, with :disabled

defer vs Alternatives

ApproachWhen to Use
deferTarget in same tree, Vue 3.5+
:disabled + nextTickOlder Vue, or more control needed
Provide/inject targetComplex cross-component coordination
body targetGlobal overlays, no layout dependency

Best Practices

✅ Do This:

<!-- Use defer when the target is in the same tree. -->
<Teleport defer to="#late-container">  <!-- ✅ -->

<!-- Prefer a stable target in index.html when possible. -->
<Teleport to="#modal-root">  <!-- ✅ -->

<!-- Combine defer with disabled for responsive behavior. -->
<Teleport defer to="#modal-root" :disabled="isMobile">  <!-- ✅ -->

<!-- Ensure the target renders in the same cycle. -->
<Teleport defer to="#target"><Modal /></Teleport>
<div id="target"></div>  <!-- ✅ -->

❌ Don’t Do This:

<!-- Don't use defer for targets in index.html. -->
<Teleport defer to="#modal-root">  <!-- ❌ Unnecessary -->

<!-- Don't expect defer to wait for Suspense. -->
<Teleport defer to="#async-target">  <!-- ❌ Target inside Suspense -->

<!-- Don't forget the target must exist in SSR. -->
<Teleport defer to="#client-only">  <!-- ❌ Breaks hydration -->

Common Pitfalls

PitfallWhy It HappensFix
defer has no effectVue < 3.5Upgrade Vue
Target still not foundTarget inside Suspense or asyncRender target earlier
Content flickersUsing old workaroundUse defer instead
Lifecycle order brokenContent mounts after parentAdjust dependencies on refs
SSR hydration mismatchTarget not in server HTMLRender target server-side

Real-World Examples

1. Basic Deferred Teleport

<Teleport defer to="#late-container">
  <Modal />
</Teleport>
<div id="late-container"></div>

2. Layout Portal Root

<Teleport defer to="#modal-root">
  <Modal v-if="open" />
</Teleport>

3. Deferred with Disabled

<Teleport defer to="#modal-root" :disabled="isMobile">
  <Modal />
</Teleport>

4. SSR Target

<body>
  <div id="app"></div>
  <div id="modal-root"></div>
</body>

5. Old Workaround

<Teleport to="#target" :disabled="!ready">
  <Modal />
</Teleport>

Visual

The Timing Problem

┌────────────────────────────────────────────────────────────┐
│  WITHOUT defer: TELEPORT RESOLVES TOO EARLY                │
│                                                            │
│  PATCH PHASE                                               │
│  ┌──────────────────────────────────────────────────┐     │
│  │  1. Teleport patches, looks for #target           │     │
│  │  2. #target does not exist yet                    │     │
│  │  3. Warning: Invalid Teleport target              │     │
│  │  4. Content not rendered                          │     │
│  └──────────────────────────────────────────────────┘     │
│                        │                                   │
│                        v                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  5. Later in patch: #target is created            │     │
│  │  6. But the teleport already failed               │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  WITH defer: TELEPORT RESOLVES AFTER MOUNT                 │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  1. Teleport patches, defers target resolution    │     │
│  │  2. #target is created during the same patch      │     │
│  │  3. Post-render effect: resolve #target           │     │
│  │  4. Content is moved to #target                   │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  The target must exist by the end of the same cycle.       │
└────────────────────────────────────────────────────────────┘

Lifecycle Order

┌────────────────────────────────────────────────────────────┐
│  LIFECYCLE WITH defer                                      │
│                                                            │
│  Parent onMounted                                          │
│         │                                                  │
│         v                                                  │
│  Deferred teleport resolves target                         │
│         │                                                  │
│         v                                                  │
│  Teleported content mounts                                 │
│         │                                                  │
│         v                                                  │
│  Teleported content onMounted                              │
│                                                            │
│  The content's onMounted fires after the parent's.         │
│  Parent refs are ready when the content mounts.            │
└────────────────────────────────────────────────────────────┘

Decision Guide

┌────────────────────────────────────────────────────────────┐
│  WHEN TO USE defer                                         │
│                                                            │
│  Is the target in index.html?                              │
│    YES → No defer needed.                                  │
│    NO  → Continue.                                         │
│                                                            │
│  Is the target rendered by Vue in the same tree?           │
│    YES → Use defer.                                        │
│    NO  → Continue.                                         │
│                                                            │
│  Is the target inside Suspense or async?                   │
│    YES → defer does not work. Render target earlier.       │
│    NO  → Use defer.                                        │
│                                                            │
│  Should the content render inline on mobile?               │
│    YES → defer + :disabled.                                │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
defer propPostpones teleport target resolution
Vue version3.5+
When to useTarget rendered by same tree
When not to useTarget in index.html, or inside Suspense
LifecycleContent mounts after parent
SSRTarget must exist in server HTML
Old workaround:disabled + nextTick
Preferred patternStable target in index.html

Key takeaways:

  • defer postpones the target lookup to the end of the mount cycle. The teleport patches normally, but resolves its target after the rest of the tree has mounted. This lets a component teleport into a target rendered later in the same tree .
  • The target must render in the same cycle. defer is a scheduling fix, not a general waiting mechanism. If the target is inside a Suspense boundary or rendered asynchronously, defer does not help .
  • Lifecycle order changes. The teleported content’s onMounted fires after the parent’s. This is usually desirable, because parent refs are ready by the time the content mounts .
  • The preferred pattern is still a stable target. A portal root in index.html requires no timing coordination. defer is the answer when the target must be rendered by Vue, not a replacement for the stable-container pattern .
  • defer composes with disabled. The two together give responsive behavior: teleport on desktop, render inline on mobile.

Remember: The defer prop is a targeted solution to a specific timing problem. It exists because Teleport resolves its target during the patch phase, before the rest of the tree has mounted. Without defer, a target rendered later in the same template cannot be found. With defer, Vue queues the target resolution to a post-render effect and finds the target once the tree is complete. The target must still render in the same mount or update cycle. For global overlays, a stable container in index.html remains the preferred pattern. defer is the tool for the cases where the target must be rendered by Vue, and the timing must be coordinated.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!