Chapter 32: Vue Teleport
Vue Teleport (<Teleport>)
This is the feature you reach for when you need to render a piece of content somewhere completely different in the DOM tree — while still keeping it logically inside your component.
In simple human words:
Teleport lets you say: “Hey Vue, please render this part of my component here in the DOM… but keep all the reactivity, events, props, scoped styles, and component lifecycle attached to me as if it was still inside my component tree.”
It’s like moving furniture to another room in the house without unpacking it — the furniture still belongs to you, it still works the same way, but it physically lives somewhere else.
Why Do We Need Teleport? Real-Life Situations
Without Teleport you would have to:
- Break component encapsulation (move modal code outside component)
- Use portals from third-party libraries (like PortalVue)
- Manually move DOM nodes with appendChild (very fragile, loses reactivity)
Common real use-cases where Teleport is almost mandatory in 2026:
- Modals / Dialogs / Popovers / Tooltips They need to be direct children of <body> (or a special #modals div) so they appear above everything (z-index, overflow:hidden problems)
- Full-screen overlays (loading spinners, notifications)
- Toasts / snackbars that should appear at the very top/bottom of viewport
- Context menus or dropdowns that break out of scrollable containers
- SSR + client-only components that need to mount at root
- Third-party integration (maps, video players, legacy widgets) that want to control their own container
Basic Syntax – <Teleport to=”selector”>
|
0 1 2 3 4 5 6 7 8 9 |
<Teleport to="body"> <!-- this content will be moved to <body> --> <div class="modal">I am actually appended to body!</div> </Teleport> |
to accepts:
- CSS selector string → “body”, “#modals”, “.app-root”
- DOM element reference → document.body
- null or empty → disables teleport (renders normally)
Real, Complete Example – Modern Modal with Teleport
Child: Modal.vue (reusable modal component)
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
<!-- src/components/Modal.vue --> <template> <Teleport to="body"> <!-- everything inside Teleport goes to <body> --> <div class="modal-overlay" @click.self="close"> <div class="modal-content" :class="{ 'slide-in': isVisible }"> <div class="modal-header"> <h2><slot name="header">Default Title</slot></h2> <button class="close-btn" @click="close">×</button> </div> <div class="modal-body"> <slot /> </div> <div class="modal-footer"> <slot name="footer"> <button @click="close">Close</button> </slot> </div> </div> </div> </Teleport> </template> <script setup lang="ts"> import { ref, onMounted, onUnmounted } from 'vue' defineProps<{ modelValue?: boolean }>() const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void (e: 'close'): void }>() const isVisible = ref(false) onMounted(() => { isVisible.value = true document.body.style.overflow = 'hidden' // prevent body scroll }) onUnmounted(() => { document.body.style.overflow = '' }) function close() { isVisible.value = false emit('close') emit('update:modelValue', false) // wait for exit animation (optional) setTimeout(() => { // actually destroy component if using v-if }, 300) } </script> <style scoped> .modal-overlay { position: fixed; inset: 0; background: rgba(0,0,0,0.6); display: flex; align-items: center; justify-content: center; z-index: 9999; } .modal-content { background: white; border-radius: 12px; width: 90%; max-width: 500px; max-height: 90vh; overflow-y: auto; box-shadow: 0 20px 50px rgba(0,0,0,0.4); transform: scale(0.8); opacity: 0; transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } .slide-in { transform: scale(1); opacity: 1; } .modal-header { padding: 1.2rem 1.5rem; border-bottom: 1px solid #e5e7eb; display: flex; justify-content: space-between; align-items: center; } .close-btn { background: none; border: none; font-size: 2rem; cursor: pointer; color: #6b7280; } .modal-body { padding: 1.5rem; } .modal-footer { padding: 1rem 1.5rem; border-top: 1px solid #e5e7eb; text-align: right; } </style> |
Parent usage – simple & clean
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<template> <div> <button @click="showModal = true"> Open Important Modal </button> <!-- v-model support via update:modelValue --> <Modal v-model="showModal" @close="showModal = false" > <template #header> <h2>⚠️ Critical Alert</h2> </template> <p>This modal is teleported to <code>&lt;body&gt;</code> so it appears above everything.</p> <p>Even if this page has overflow:hidden or z-index chaos.</p> <template #footer> <button @click="showModal = false">Understood</button> </template> </Modal> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import Modal from '@/components/Modal.vue' const showModal = ref(false) </script> |
Important Teleport Behaviors & Gotchas (2026 Details)
| Question / Gotcha | Answer / Behavior |
|---|---|
| Where can to point? | Any existing DOM element (usually “body”, “#modals”, “#app” root) |
| What happens if target doesn’t exist? | Teleport content is not rendered until target appears (safe) |
| Does <Teleport> create wrapper? | No — children are moved directly into target (no extra DOM node) |
| Styles — scoped or global? | Scoped styles still work — Vue keeps data-v-xxx on teleported elements |
| Events — still bubble to component? | Yes — event handling stays attached to Vue component tree |
| Multiple Teleports to same target? | Yes — all children are appended in order |
| SSR / hydration? | Works perfectly — teleported content appears client-side only |
| Disable teleport? | <Teleport :to=”null”> or disabled prop (Vue 3.3+) |
| Transition support? | Yes — <Transition> + <Teleport> works beautifully |
Pro Tips from Real 2026 Projects
- Create a central #modals div in index.html or App.vue → <div id=”modals”></div>
- Use to=”#modals” instead of to=”body” → better organization & z-index control
- Combine with <Transition> for beautiful enter/leave animations
- Use multiple named Teleports inside one component (header to #header-slot, content to #main-content)
- For toasts/notifications → create a global <Teleport to=”body”> manager component
Your Mini Practice Task
- Create Tooltip.vue that uses <Teleport to=”body”> to render popup
- Make it appear near the trigger element (use :style with position calculation)
- Add v-if + <Transition> for smooth fade-in/out
Any part still unclear? Want me to show:
- Teleport + <Transition> for modal animation?
- Multiple Teleports in one component?
- Teleport to a custom #portals container?
- Real toast/notification system with Teleport?
Just tell me — we’ll build the next beautiful example together 🚀
Happy teleporting from Hyderabad! 💙
