A Vue 3 app warns "Failed to locate Teleport target with selector" for a container another component renders; why does this happen, and how does `defer` fix it?
answer
- timing of the target lookup
- same render pass, not yet attached
- 3.5 prop, works like mounted
- production stays silent
basics
~20 sA Teleport resolves its to selector when it mounts, and a container Vue renders in the same pass is not yet in the document. Vue 3.5's defer delays the lookup until the rest of that mount or update tick has mounted.
solid answer
~40 s`<Teleport>` looks up its `to` selector with `querySelector` at the moment it mounts. If the target is rendered by Vue in the same render pass, for example a `<div id="toolbar-slot">` in a layout that mounts alongside it, that element is not attached to the document yet, so the lookup fails. In development Vue warns and names the selector; in production it logs nothing and the content simply never appears. Fixes: put the target in `index.html` outside the app; or, in Vue 3.5+, add `defer`, which queues target resolution as a post-render effect so it runs after the rest of the same mount/update tick, much like `onMounted`. `defer` does not wait for a target that appears in a later tick.
code
vue · 15 lines<script setup lang="ts">
import { ref, onMounted } from 'vue'
const targetReady = ref(false)
onMounted(() => {
targetReady.value = true
})
</script>
<template>
<div id="late-slot"></div>
<Teleport v-if="targetReady" to="#late-slot">
<span>Mounted after the slot exists</span>
</Teleport>
</template>go deeper
Know that a Teleport target has to exist in the page when the Teleport mounts, and that body or a div in index.html are safe choices.
Explain that the lookup is a querySelector at mount time and that a target Vue renders in the same pass is not attached yet, which is why the warning fires.
Diagnose the refresh-only variant, note that production is silent, and pick between a static target, defer for same-tick targets and a delayed mount for later ones.
Standardise overlay and slot targets as static containers or a single layout convention, and add a render test so a silently missing teleport cannot ship.
## The symptom A layout renders a slot for page-specific toolbar buttons, and pages teleport into it: ```vue <!-- AppLayout.vue --> <template> <header><div id="toolbar-slot"></div></header> <main><RouterView /></main> </template> <!-- SomePage.vue --> <template> <Teleport to="#toolbar-slot"><button>Export</button></Teleport> </template> ``` On first load the console shows a warning beginning `Failed to locate Teleport target with selector "#toolbar-slot"`, usually followed by `Invalid Teleport target on mount`, and the Export button is missing. ## Why it happens 1. When a `<Teleport>` mounts, it resolves `to` immediately, using the renderer's `querySelector`, which in the browser is `document.querySelector`. 2. During a render pass, Vue builds new elements and inserts them into their parents as it goes. A subtree created in that pass is typically **not attached to the document** until its root is inserted, so a target rendered by Vue in the same pass is not findable yet, even if it appears earlier in the template. 3. If the lookup returns nothing and the Teleport is enabled, Vue **does not mount the content at all**. There is no fallback to rendering in place. Vue's development warning spells out the rule: the target must exist before the component is mounted, cannot be rendered by the component itself, and ideally sits outside the whole Vue component tree. ## Why it is easy to miss - Both warnings are **development-only**. A production build prints nothing; the teleported content is just absent. - The bug can be timing-dependent: after client-side navigation the layout is already mounted, so the target exists and the Teleport works. Only a hard refresh on that page fails. ## The fixes | Fix | When to use it | Caveat | |---|---|---| | Put the target in `index.html` outside the app element | Global layers: modals, toasts | Only works for app-wide containers | | Use `to="body"` | Simple full-screen overlays | Loses grouping; everything lands in body | | Add `defer` (Vue 3.5+) | Target rendered by Vue in the **same** mount/update tick | Does not wait for later ticks | | Mount the Teleport only after the target exists, e.g. `v-if` on a flag set in the owner's `onMounted` | Pre-3.5 code, or targets that appear later | Extra render; the flag lives in the right component | ## How `defer` works `<Teleport defer to="#toolbar-slot">` makes the Teleport insert its placeholders immediately but postpone resolving and mounting into the target. The mount is queued as a **post-render effect**, the same queue that runs `mounted` hooks, so it runs after every other part of the current mount or update has been attached. The Vue docs describe it as working similarly to the `mounted` lifecycle hook. The limit matters in interviews: the target must be rendered **in the same tick** as the Teleport. If the container only appears a second later, say after an async fetch resolves and a `v-if` flips, the deferred lookup still fails and still warns. ## Diagnosing checklist - Read the selector in the warning and confirm it matches an element that exists at that moment. - Ask who renders the target and when: static HTML, the same tick, or a later tick. - Remember that production gives no signal, so add a test that mounts the page and asserts the teleported content is present. - For components that must render without their target, such as in tests, bind `disabled` to keep the content inline. ## Choosing a target strategy for an app A team that relies on teleporting into Vue-rendered containers should make the timing rules explicit rather than rediscovering them page by page: - **App-wide layers** such as modals, toasts and tooltips belong in static containers in `index.html`, next to the app's mount element. They exist before anything mounts, so no timing question arises. - **Layout slots** that a layout renders and pages fill, such as a toolbar or a sidebar area, are the case `defer` was added for. Mark every Teleport into such a slot with `defer`, and keep the slot unconditionally rendered by the layout so it always exists in the same tick. - **Targets that appear conditionally**, for instance behind a `v-if` that depends on fetched data, are the fragile case. Either render the target unconditionally and hide it, or make the Teleport's own mount conditional on the same state. The common thread is that Teleport never watches the document for a target; it resolves `to` when it mounts and again only when `to` changes. ## Summary Teleport's target lookup is a one-shot `querySelector` at mount time. Targets outside the app always work; targets rendered by Vue need either to exist from an earlier tick or, since 3.5, `defer` for the same tick. Anything later needs the Teleport itself to mount later.
- The target is rendered after an API call resolves; will `defer` help?No. `defer` only postpones the lookup to the end of the current mount or update tick. A target that appears in a later tick, after an async fetch flips a `v-if`, is still missing when the deferred lookup runs, and Vue warns as before. Mount the Teleport only once the target is rendered, or move the target to a container that always exists.
- Why does the teleported toolbar work after client-side navigation but vanish on a hard refresh?On navigation the layout is already mounted, so its target element is in the document when the new page's Teleport mounts. On a hard refresh the layout and the page mount in the same pass, and the target is not attached yet. It is a timing bug, which `defer` fixes when both render in the same tick.
saying these in an interview costs you the question
- defer makes Teleport wait until the target appears, however late
- A missing target makes Teleport fall back to rendering in place
- The missing-target warning also appears in production builds
- Placing the target earlier in the same template always fixes the lookup
- defer has been available since Vue 3.0