Suggest an editImprove this articleRefine the answer for “Teleport in Vue.js”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Teleport** is a Vue 3 component that renders its content into any DOM node outside the component tree. ```vue <Teleport to="body"> <div v-if="show" class="modal">Modal content</div> </Teleport> ``` **Key:** The DOM moves to the target, but reactivity, props, and events stay tied to the original component instance.Shown above the full answer for quick recall.Answer (EN)Image**Teleport** is a built-in Vue 3 component that renders its content into any DOM node you choose, completely outside the component tree that contains it. ## Theory ### TL;DR - Teleport is like mail forwarding: you write the content in one place, Vue delivers the DOM to a different address - Content moves to the `to` target but stays Vue-managed (reactive, props, events, provide/inject all work normally) - The Vue component tree does NOT change, only the DOM position changes - Use it when parent CSS (overflow, z-index stacking context) breaks overlay layouts - Skip it when `position: fixed` alone solves the problem without style conflicts ### Quick example ```vue <template> <button @click="show = true">Open</button> <Teleport to="body"> <div v-if="show" class="modal" @click.self="show = false"> Click outside to close </div> </Teleport> </template> <script setup> import { ref } from 'vue' const show = ref(false) </script> <style scoped> .modal { position: fixed; inset: 0; background: rgba(0,0,0,0.5); } </style> ``` The modal renders as a direct child of `<body>`, not inside the button's parent component. The `v-if` and the click handler still react to `show` from the original component scope. ### Key difference Teleport moves the actual rendered DOM to your `to` selector during mount. The component instance and reactivity stay in the original Vue tree. So `v-model`, props, events, and provide/inject all work as expected. CSS inheritance, though, stops at the new target's parent, which means the teleported content is free from whatever styles the original parent had. ### When to use - **Modals and dialogs:** `to="body"` sidesteps z-index problems caused by parent stacking contexts - **Tooltips and popovers:** escapes `overflow: hidden` clipping in ancestor containers - **Notifications:** `to="#notifications"` stacks them without affecting layout flow - **Rich-text editor dropdowns:** avoids toolbar menus being clipped by the editor wrapper Most z-index nightmares I have seen in Vue dashboards came from a modal stuck inside a card component with `overflow: hidden`. One `<Teleport to="body">` and the problem was gone. Skip Teleport when `position: fixed` alone handles it. ### How Vue handles Teleport internally During mount, Vue's renderer extracts the Teleport's child VNodes, calls `document.querySelector(to)` to find the target, then appends the real DOM with `appendChild` or `insertBefore`. Reactivity updates still target the relocated nodes through an internal vnode flag. On unmount, Vue removes the DOM from the target automatically. Setting `:disabled="true"` skips relocation entirely and renders content inline. ### Common mistakes **1. Teleporting to a selector that does not exist** ```vue <!-- Wrong: content becomes invisible --> <Teleport to=".missing-class"> <div class="modal">...</div> </Teleport> ``` `querySelector` returns null and the content renders inline but may be completely invisible. Use `body` or a guaranteed element. This is the most reported Teleport issue on StackOverflow. **2. Expecting scoped styles to follow teleported content** ```vue <!-- Wrong: .modal style has no effect after teleport --> <Teleport to="body"> <div class="modal">...</div> </Teleport> <style scoped> .modal { opacity: 1; } /* CSS stays scoped to original mount point */ </style> ``` Scoped styles add a `data-v-xxxxx` attribute to both the DOM node and the CSS selector. The teleported node keeps the attribute, but body-level styles can override yours. Fix: use a global `<style>` block or `:deep()` for anything that needs to apply after relocation. **3. Forgetting SSR** On the server there is no `body` to query. Vue renders Teleport content inline during SSR. If the client then teleports, you get a hydration mismatch error. Disable Teleport until after mount: ```vue <script setup> import { ref, onMounted } from 'vue' const isMounted = ref(false) onMounted(() => { isMounted.value = true }) </script> <template> <Teleport to="body" :disabled="!isMounted"> <div class="toast">Saved!</div> </Teleport> </template> ``` **4. Nested Teleports without disabled in SSR** An inner Teleport resolves its `to` selector relative to the outer Teleport's DOM target, not the original template position. In SSR this creates hydration errors. Disable inner Teleports on the server the same way as above. ### Real-world usage - **Vuetify 3:** all `v-dialog` and `v-menu` components use `<Teleport to="body">` - **Element Plus:** `ElMessage` and `ElNotification` teleport to a custom `#app` root - **Naive UI:** `n-message` appends directly to `document.body` - **Quasar:** `QDialog` uses a body target under the hood React has `createPortal` for the same purpose, but it requires manual cleanup. Vue Teleport removes the DOM automatically when the component unmounts. ### Follow-up questions **Q:** What is the difference between Teleport and `position: fixed`? **A:** `position: fixed` still respects the parent stacking context when the parent has `transform`, `filter`, or `will-change` set. Teleport physically moves the DOM to the target element, so only that target's stacking context applies. **Q:** How does Teleport handle multiple matches for the `to` selector? **A:** It appends to the first element returned by `querySelector`. Use unique IDs when you need predictable placement. **Q:** What happens during SSR? **A:** Vue renders Teleport content inline on the server. On the client it relocates the DOM during mount. Tie `:disabled` to an `onMounted` flag to keep server and client output in sync. **Q:** Does Teleport affect performance? **A:** Minimal. Each Teleport does one DOM move at mount time. Avoid creating hundreds of them separately; batch things like notifications into one shared target container instead. **Q:** (Senior) Why does a nested Teleport resolve `to` relative to the outer Teleport's target? **A:** The inner Teleport's `to` is evaluated against the live DOM at mount time. Since the outer Teleport already moved its subtree to a new location, the inner Teleport resolves against that relocated DOM, not the original component tree position. ## Examples ### Basic: modal that escapes container overflow ```vue <template> <div class="card" style="overflow: hidden; position: relative;"> <button @click="open = true">Edit</button> <Teleport to="body"> <div v-if="open" class="overlay" @click.self="open = false"> <div class="dialog"> <h2>Edit item</h2> <button @click="open = false">Close</button> </div> </div> </Teleport> </div> </template> <script setup> import { ref } from 'vue' const open = ref(false) </script> <style> .overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.4); display: flex; align-items: center; justify-content: center; } .dialog { background: white; padding: 24px; border-radius: 8px; } </style> ``` Without Teleport, `overflow: hidden` on `.card` clips the modal entirely. Teleport moves the dialog to `<body>` and it renders correctly. The style is global here, not scoped, so it applies after relocation. ### Intermediate: notification toast with SSR-safe pattern ```vue <!-- NotificationToast.vue --> <template> <Teleport to="#notifications" :disabled="!mounted"> <Transition name="fade"> <div v-if="visible" class="toast" role="alert">{{ message }}</div> </Transition> </Teleport> </template> <script setup> import { ref, onMounted } from 'vue' defineProps({ message: String }) const visible = ref(true) const mounted = ref(false) onMounted(() => { mounted.value = true setTimeout(() => { visible.value = false }, 3000) }) </script> ``` ```html <!-- index.html --> <div id="app"></div> <div id="notifications"></div> ``` Multiple toasts can teleport to the same `#notifications` container and stack in mount order. The `Transition` still works because Vue manages animation state on the original component instance, not the relocated DOM. The `mounted` flag prevents SSR hydration mismatches. ### Advanced: reusable modal component with SSR and lifecycle handled ```vue <!-- Modal.vue --> <template> <Teleport to="body" :disabled="!isMounted"> <Transition name="fade"> <div v-if="isOpen" class="modal-overlay" @click.self="emit('close')"> <div class="modal-content" role="dialog"> <button class="close-btn" @click="emit('close')">x</button> <slot /> </div> </div> </Transition> </Teleport> </template> <script setup> import { ref, onMounted } from 'vue' defineProps({ isOpen: Boolean }) const emit = defineEmits(['close']) const isMounted = ref(false) onMounted(() => { isMounted.value = true }) </script> <style> .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; } .modal-content { background: white; padding: 24px; border-radius: 8px; max-width: 500px; position: relative; } .close-btn { position: absolute; top: 12px; right: 12px; background: none; border: none; font-size: 20px; cursor: pointer; } </style> ``` This pattern covers the three things that trip up developers in production. The Teleport escapes parent `overflow` and z-index. The `Transition` animates correctly because Vue's animation system operates at the component level. And `:disabled="!isMounted"` keeps SSR and client rendering in sync. Vuetify 3 uses the same approach for all `v-dialog` components.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.