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
| Scenario | Recommended |
|---|---|
Target in index.html | No defer needed |
| Target rendered by the same tree | defer |
Target inside Suspense | Not supported by defer |
| Target on mobile should be inline | defer + :disabled |
| Target must persist across routes | body 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
| Approach | Timing | Flicker | Layout preserved | Complexity |
|---|---|---|---|---|
defer | Same cycle | No | Yes | Low |
:disabled + nextTick | Next cycle | Possible | Yes | Medium |
| Provide/inject target | Any | No | Yes | High |
body target | Immediate | No | No | Low |
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
| Aspect | Value |
|---|---|
| Type | Boolean |
| Vue version | 3.5+ |
| Effect | Postpones target resolution to the end of the mount/update cycle |
| Target requirement | Must render in the same cycle |
| Lifecycle | Content mounts after parent |
| SSR | Target must exist in server-rendered HTML |
When to Use defer
| Scenario | defer? |
|---|---|
| Target in index.html | No |
| Target rendered by same tree | Yes |
| Target inside Suspense | No (does not work) |
| Target rendered asynchronously | No (does not work) |
| Mobile should render inline | Yes, with :disabled |
defer vs Alternatives
| Approach | When to Use |
|---|---|
defer | Target in same tree, Vue 3.5+ |
:disabled + nextTick | Older Vue, or more control needed |
| Provide/inject target | Complex cross-component coordination |
body target | Global 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
| Pitfall | Why It Happens | Fix |
|---|---|---|
| defer has no effect | Vue < 3.5 | Upgrade Vue |
| Target still not found | Target inside Suspense or async | Render target earlier |
| Content flickers | Using old workaround | Use defer instead |
| Lifecycle order broken | Content mounts after parent | Adjust dependencies on refs |
| SSR hydration mismatch | Target not in server HTML | Render 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
| Item | Value |
|---|---|
defer prop | Postpones teleport target resolution |
| Vue version | 3.5+ |
| When to use | Target rendered by same tree |
| When not to use | Target in index.html, or inside Suspense |
| Lifecycle | Content mounts after parent |
| SSR | Target must exist in server HTML |
| Old workaround | :disabled + nextTick |
| Preferred pattern | Stable target in index.html |
Key takeaways:
deferpostpones 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.
deferis a scheduling fix, not a general waiting mechanism. If the target is inside a Suspense boundary or rendered asynchronously,deferdoes not help . - Lifecycle order changes. The teleported content’s
onMountedfires 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.htmlrequires no timing coordination.deferis the answer when the target must be rendered by Vue, not a replacement for the stable-container pattern . defercomposes withdisabled. 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!