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?
answer
- where the DOM nodes land
- logical owner stays the same
- selector string or element
- later mounts append after earlier
basics
~20 sVue'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 sA 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<body>
<div id="app"></div>
<div id="modals"></div>
<script type="module" src="/src/main.ts"></script>
</body>go deeper
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.
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.
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.
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