Chapter 50: Vue Teleport Component
The <Teleport> component
This is not just another animation or transition helper — it’s a structural DOM tool that solves a very specific but extremely common pain point.
What problem does <Teleport> solve?
Imagine you’re building a modal, dialog, toast notification, full-screen overlay, tooltip that needs to break out of its container, or any UI element that must appear on top of everything else in the visual stacking order (high z-index).
In a typical component tree, your modal lives deep inside some layout:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
<body> <div id="app"> <Header /> <main> <Dashboard> <Widget> <Button @click="showModal = true">Open Modal</Button> <Modal v-if="showModal">…</Modal> ← here </Widget> </Dashboard> </main> </div> </body> |
Problems when modal is nested like this:
- It inherits parent’s overflow: hidden → modal gets cut off
- It inherits parent’s position: relative or low z-index → modal appears behind header/sidebar/other elements
- It inherits parent’s transform, filter, perspective → modal animations break (creates new stacking context)
- Accessibility tools (screen readers) get confused by deep nesting
Solution before Vue 3: manually move the modal DOM node to <body> using appendChild in mounted and removeChild in beforeUnmount — ugly, error-prone, breaks reactivity if not careful.
Vue 3 solution: <Teleport>
It lets you keep the modal logically inside your component (so it has access to props, events, reactivity, scoped styles, slots, etc.) while physically moving its DOM subtree to any other place in the document — usually directly under <body>.
Basic Syntax
|
0 1 2 3 4 5 6 7 8 9 |
<Teleport to="body"> <!-- everything inside here is rendered inside <body> --> <div class="my-modal">I look like I'm deep in the component tree… but I'm actually at the end of body!</div> </Teleport> |
to can be:
- CSS selector string → “body”, “#modals”, “.app-root > .overlay-layer”
- Actual DOM element → document.body, document.getElementById(‘portals’)
- null → disables teleport (renders normally)
Real-World Example – Modern Modal with Teleport
File: src/components/Modal.vue
|
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 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 |
<template> <!-- Teleport moves this entire tree to body --> <Teleport to="body"> <Transition name="modal"> <div v-if="isOpen" class="modal-overlay" @click.self="close"> <div class="modal-content" @click.stop> <div class="modal-header"> <h2><slot name="header">Confirmation</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">Cancel</button> <button class="confirm" @click="confirm">Confirm</button> </slot> </div> </div> </div> </Transition> </Teleport> </template> <script setup lang="ts"> import { ref, watch } from 'vue' const props = defineProps<{ modelValue: boolean }>() const emit = defineEmits<{ (e: 'update:modelValue', value: boolean): void (e: 'confirm'): void }>() const isOpen = ref(props.modelValue) watch(() => props.modelValue, (val) => { isOpen.value = val }) function close() { emit('update:modelValue', false) } function confirm() { emit('confirm') close() } // Optional: prevent body scroll when modal is open watch(isOpen, (open) => { document.body.style.overflow = open ? 'hidden' : '' }) </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.9); opacity: 0; transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } .modal-enter-active .modal-content, .modal-leave-active .modal-content { transition: all 0.3s cubic-bezier(0.34, 1.56, 0.64, 1); } .modal-enter-to .modal-content, .modal-leave-from .modal-content { 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; } .confirm { background: #3b82f6; color: white; border: none; padding: 0.6rem 1.2rem; border-radius: 6px; margin-left: 0.8rem; cursor: pointer; } </style> |
Parent usage – very 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 37 38 39 40 |
<template> <div> <button @click="showModal = true"> Open Confirmation Modal </button> <Modal v-model="showModal" @confirm="handleConfirm" > <template #header> <h2>⚠️ Delete Project?</h2> </template> <p>This action cannot be undone. All data will be permanently lost.</p> <template #footer> <button @click="showModal = false">Cancel</button> <button class="confirm" @click="handleConfirm">Yes, Delete</button> </template> </Modal> </div> </template> <script setup lang="ts"> import { ref } from 'vue' import Modal from '@/components/Modal.vue' const showModal = ref(false) function handleConfirm() { console.log('User confirmed deletion!') showModal.value = false } </script> |
What Actually Happens in the DOM
Even though <Modal> is nested deep inside your component tree, the rendered output looks like:
|
0 1 2 3 4 5 6 7 8 9 10 11 12 13 |
<body> ... your normal app content ... <!-- Teleport target --> <div class="modal-overlay"> <div class="modal-content">…</div> </div> </body> |
→ No wrapper div from <Teleport> itself — children are directly appended to the target.
Important Behaviors & Gotchas (2026 Details)
| Question / Gotcha | Answer / Behavior |
|---|---|
| Where can to point? | Any existing DOM element — usually “body”, “#modals”, “.portal-layer” |
| What if target doesn’t exist yet? | Content stays hidden until target appears (safe) |
| Does Teleport create extra wrapper? | No — children are moved directly into target |
| Scoped styles — do they still work? | Yes — Vue keeps data-v-xxx attributes on teleported elements |
| Events — do they still work normally? | Yes — bubbling & handling stay in component tree |
| Multiple Teleports to same target? | Yes — appended in DOM order |
| SSR / hydration? | Works fine — teleported content only appears client-side |
| Disable teleport temporarily? | <Teleport :disabled=”true”> or :to=”null” |
| Transition support? | Perfect — <Transition> inside <Teleport> works beautifully |
Pro Tips from Real Projects (Hyderabad 2026)
-
Create a central portal container in index.html or App.vue
HTML0123456789<body><div id="app"></div><div id="modals"></div> <!-- ← all modals/toasts go here --></body>Then always to=”#modals” — better z-index control, easier debugging
-
Combine with <Transition> for beautiful modal animations
-
Use multiple named Teleports inside one component if needed
vue0123456789101112<Teleport to="#header-slot"><div>Custom header content</div></Teleport><Teleport to="#main-overlay"><div>Overlay content</div></Teleport> -
For toasts/notifications → create a global <ToastContainer> that uses Teleport internally
-
Accessibility tip — add aria-modal=”true” and focus trap inside modal
Your Mini Practice Task
- Create Modal.vue exactly as above (with Teleport to “body”)
- Use it from a deeply nested component (inside a dashboard widget)
- Add <Transition name=”modal”> around the overlay/content
- Check DevTools → confirm modal is directly under body, not nested
Any part confusing? Want full examples for:
- Toast/notification system using Teleport?
- Teleport + <Transition> + focus trap for accessibility?
- Multiple Teleports in one component?
- Teleport to a custom portal div vs body?
Just tell me — we’ll build the next clean, production-ready modal/toast system together 🚀
