skip to content

Teleport

Teleport renders part of a component's template into another DOM node, such as body for a modal, while it stays in the component tree. Interviewers probe the to, disabled and defer props.

part ofVue.jsoverview, primer and where to startread it →
on this pageshow

explore

questions

4

In Vue 3, how does the built-in `<Teleport>` fix a modal clipped inside a deeply nested scrolling container, and what does its `to` prop accept?

level: juniorimportance: must knowfreq 62%

answer

  1. where the DOM nodes land
  2. logical owner stays the same
  3. selector string or element
  4. later mounts append after earlier

basics

~20 s

Vue's <Teleport> renders its slot content into another DOM node, typically body, so ancestor overflow, transforms and stacking contexts stop constraining the modal. Its to prop takes a CSS selector string or an actual DOM element.

solid answer

~40 s

A modal written inside a component sits wherever that component sits in the DOM, so an ancestor with `overflow` clipping, a `transform`/`filter`/`perspective` that turns `position: fixed` into positioning relative to that ancestor, or a low stacking context can clip or bury it. Wrapping the modal markup in `<Teleport to="body">` makes Vue insert those nodes into `body` instead, while the markup, its state and its handlers stay in the same SFC. `to` accepts a CSS selector (resolved with `querySelector`, first match) or a real DOM element. Several teleports to one target simply append in mount order, and when the owning component unmounts, Vue removes the teleported nodes too.

code

html · 5 lines
html
<body>
  <div id="app"></div>
  <div id="modals"></div>
  <script type="module" src="/src/main.ts"></script>
</body>

go deeper

for a junior

Recall that Teleport moves rendered markup to another DOM node such as body, and that to takes a selector or an element. The modal is the example to give.

for a middle

Explain why nested modals break: clipping, containing blocks created by transforms, and stacking contexts. Then show that only the DOM position changes while state and handlers stay in the SFC.

for a senior

Recommend a dedicated target in index.html, describe append order for stacked modals, and flag what changes with DOM position, such as click-outside logic and descendant CSS selectors.

for a principal

Frame Teleport as a team convention: a few named overlay targets, one modal primitive owning focus and stacking, and a rule that feature code never positions overlays inside scrolling containers.

## The problem Teleport solves A Vue component renders its template where the component itself sits in the DOM. That is usually what you want, but a **modal** is the classic exception. Its open/close state and its trigger button belong to one component, yet visually it must cover the whole viewport. When that component lives deep inside a scrolling panel, several CSS facts work against it: - an ancestor with `overflow: hidden` or `overflow: auto` clips an absolutely positioned descendant to its box; - an ancestor with `transform`, `perspective` or `filter` becomes the containing block for `position: fixed`, so a "fixed" modal is positioned relative to that ancestor rather than the viewport; - the modal's `z-index` only competes inside its ancestors' **stacking context**, so an unrelated sibling with a higher stacking context can cover it. Moving the modal's markup up into the app root would fix the layout but scatter one feature across unrelated files. ## What `<Teleport>` does `<Teleport>` is one of the built-in components shipped in the `vue` package. It renders its slot content into a different DOM node while the content stays part of the same component: ```vue <script setup lang="ts"> import { ref } from 'vue' const open = ref(false) </script> <template> <button @click="open = true">Delete account</button> <Teleport to="body"> <div v-if="open" class="modal"> <p>Are you sure?</p> <button @click="open = false">Cancel</button> </div> </Teleport> </template> ``` The button stays inside the scrolling panel; the `.modal` element is inserted as a child of `body`. The `open` ref, the click handlers and the component's reactivity are exactly what they would be without the Teleport. ## What `to` accepts | Value of `to` | How Vue uses it | |---|---| | A CSS selector string such as `"body"`, `"#modals"`, `".layer"`, `"[data-teleport]"` | Resolved with the renderer's `querySelector`, so the **first** matching element wins | | An actual DOM element (`HTMLElement`) | Used directly as the container | The prop is required. If `to` changes while the Teleport is enabled, Vue moves the already-rendered content to the new target rather than recreating it. ## What you see in the DOM 1. In the original position, Vue leaves two placeholder nodes: in development they are the comments `<!--teleport start-->` and `<!--teleport end-->`; in production they are empty text nodes. 2. Inside the target, the teleported elements are appended. 3. When several `<Teleport>`s point at the same target, each mount **appends** after the earlier ones, so a second modal opened later ends up after the first inside `#modals` and, with equal `z-index`, paints on top of it. 4. When the owning component unmounts, Vue removes the teleported nodes from the target as well; nothing is left behind in `body`. ## Practical guidance - Prefer a dedicated container placed in `index.html` next to the app's mount element, such as `<div id="modals"></div>`, over `body` itself: the container is guaranteed to exist before the app mounts and keeps overlays grouped. - Keep the trigger and the modal in the same SFC; that locality is the whole reason to teleport instead of lifting state up. - Teleport changes only the **DOM position**. Props, emitted events and injections still flow through the component tree, and Vue Devtools still shows the modal under its parent. - Because DOM position changes, anything that depends on DOM ancestry changes too: native event bubbling, descendant CSS selectors and `contains()`-based click-outside checks now see `body`, not the panel. - Animating the teleported modal is a separate concern handled by wrapping content in `<Transition>`. ## Teleport compared with the alternatives Before `<Teleport>` existed, teams escaped nested layouts in other ways, and interviewers sometimes ask why Teleport is preferred: | Approach | What it costs | |---|---| | Render the modal in `App.vue` and drive it through shared state | The feature's markup, state and handlers are split across distant files | | Mount a second Vue app into `body` for each modal | The modal is cut off from the first app's component tree, so injections, plugins and parent listeners no longer reach it | | Move the node yourself with `appendChild` in `onMounted` | Vue no longer knows where its nodes are; patches and unmounts can misbehave | | `<Teleport to="body">` | Nothing structural: one SFC, one component tree, only the DOM position changes | The last row is the reason Teleport is the default answer: it is the only option that keeps the component relationship intact while Vue itself manages the moved nodes, including removing them on unmount. ## Summary `<Teleport>` separates **where markup is written** from **where its nodes are inserted**. The component keeps ownership of state and lifecycle; only the rendered DOM moves. That is why it is the standard Vue answer for modals, toasts and dropdowns that must escape clipping or stacking inside a nested layout.

  • Two instances of a reusable modal both teleport to #modals; which one appears on top?
    Multiple teleports to one target append in mount order, so the modal mounted later is placed after the earlier one inside `#modals`. With equal `z-index` and positioning, later siblings paint on top, so the most recently opened modal covers the first one. If you need a different order, control it with explicit `z-index` values rather than relying on mount timing.
  • What does Vue leave in the component's original DOM position after teleporting?
    Two placeholder nodes marking where the Teleport sits in the template. In development they are the comments `teleport start` and `teleport end`, which makes the Teleport easy to spot in devtools; in production they are empty text nodes. If `disabled` is later set, the content is moved back between those markers.

A mail forwarding address: letters are still addressed to you and you still own them, but they are physically delivered to a different building.

saying these in an interview costs you the question

  • Teleport creates a separate Vue app, so the modal loses its parent's state
  • The to prop only accepts an element id, not a general CSS selector
  • A second teleport to the same target replaces the first one's content
  • Teleported nodes stay in body after the owning component unmounts
  • Setting a huge z-index always fixes a modal buried in a nested container
open as a page

In Vue 3, when a child component renders inside `<Teleport to="body">`, which relationships follow the component tree and which follow the DOM?

level: middleimportance: must knowfreq 52%

basics

~20 s

Vue relationships follow the component tree: props, emitted events, injections, Devtools nesting and reactivity work as without Teleport. Anything the browser computes from DOM position follows the target: native event bubbling, ancestor-based CSS selectors and contains() checks.

open as a page

In Vue 3, how does `<Teleport>`'s `disabled` prop render content inline on mobile but in body on desktop, and does state survive toggling?

level: middleimportance: should knowfreq 40%

basics

~10 s

Binding :disabled="isMobile" on <Teleport> renders the content in place while true and in the target while false. Toggling moves the existing DOM nodes, so component state, input values and lifecycle are preserved; nothing remounts.

open as a page

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?

level: seniorimportance: should knowfreq 34%

basics

~20 s

A 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.

open as a page