Vue.js course Β· Module 8: Slots & Dynamic Components
Teleport
In this lesson6
In NOVA LAB, the alarm system displays an alert on the station's main screen, no matter which module triggered it. Vue has <Teleport> - a mechanism for rendering content in a different place in the DOM, outside the component hierarchy.
The Problem
Modals, toasts and popups logically belong to a component, but visually they should be rendered at the top level of the DOM (e.g. directly in <body>) to avoid problems with z-index, overflow: hidden and positioning. Imagine an oxygen alarm nested in a module panel that has overflow: hidden: the modal gets clipped to the size of the panel. If some ancestor has a transform property, even position: fixed stops being relative to the browser window. Moving the modal to App.vue would solve the CSS problem, but it would tear the logic apart: the alarm state lives in the oxygen module, after all.
Teleport Basics
Teleport lets you keep the modal's code where its logic is and still render its HTML somewhere else. In the panel below, the button sets showModal, and the whole modal is wrapped in <Teleport to="body">:
1<script setup>
2import { ref } from 'vue'
3const showModal = ref(false)
4</script>
5
6<template>
7 <div class="module-panel">
8 <h2>Oxygen Module</h2>
9 <button @click="showModal = true">Show Alert</button>
10
11 <Teleport to="body">
12 <div v-if="showModal" class="modal-overlay">
13 <div class="modal">
14 <h3>Critical Alert!</h3>
15 <p>Oxygen level below 20%</p>
16 <button @click="showModal = false">Dismiss</button>
17 </div>
18 </div>
19 </Teleport>
20 </div>
21</template>The content inside <Teleport> is physically rendered in <body>, but logically it belongs to the component - it has access to its data and props. Only the address in the DOM changed: passing props, emitting events and inject work exactly as before, and Vue Devtools still show the modal as a child of the panel.
The to Attribute
The to attribute accepts a CSS selector, and if needed also an actual DOM element. You will most often see three variants:
1<!-- To body -->
2<Teleport to="body">...</Teleport>
3
4<!-- To an element with an ID -->
5<Teleport to="#modal-container">...</Teleport>
6
7<!-- To an element with a class -->
8<Teleport to=".notifications">...</Teleport>The target must already exist in the DOM when the Teleport is mounted. Ideally it is an element outside the whole Vue application, for example a <div id="modal-container"> added to index.html next to #app. If you point at a selector that doesn't exist, Vue prints a warning and the content doesn't appear.
Deferred Teleport
Since Vue 3.5, Teleport has a defer attribute. Thanks to it, the target can be rendered by the same application a bit further down in the template, because Vue looks for it only after the rest of the tree has mounted:
1<template>
2 <Teleport defer to="#station-log">
3 <p>New entry in the station log</p>
4 </Teleport>
5
6 <!-- this element is rendered further down in the template -->
7 <div id="station-log"></div>
8</template>Without defer, the same code would look for #station-log before the element exists. With defer, the target only has to appear in the same render cycle as the Teleport.
Disabled Teleport
You can disable teleporting dynamically with the disabled attribute. Here the isMobile variable makes the popup render in place on a phone and in <body> on a desktop:
1<script setup>
2import { ref } from 'vue'
3const isMobile = ref(false)
4</script>
5
6<template>
7 <!-- On mobile render locally, on desktop in body -->
8 <Teleport to="body" :disabled="isMobile">
9 <div class="popup">
10 <p>Popup content</p>
11 </div>
12 </Teleport>
13</template>When disabled is true, the content renders in its original place in the component tree. Changing the flag at runtime moves the same element back and forth, without creating it anew.
Multiple Teleports to One Target
You can have several <Teleport> components rendering into the same element - the content is appended in mounting order, so later ones end up after earlier ones:
1<template>
2 <Teleport to="#notifications">
3 <div class="toast">Alert 1</div>
4 </Teleport>
5
6 <Teleport to="#notifications">
7 <div class="toast">Alert 2</div>
8 </Teleport>
9</template>The #notifications container shows "Alert 1" first and "Alert 2" below it. No Teleport overwrites the content of the others, which is why this pattern works great for a stack of notifications.
My advice: add a separate container for modals to index.html next to #app, and teleport all alarm windows there. body works too, but a dedicated container keeps the page layers tidy. In the project at the end of the module, Teleport will display a critical alarm triggered from the cargo list. In the editor below, the oxygen module triggers such an alarm: the modal lands in <body>, even though its state lives in App.vue.
Remember: Teleport moves the alarm screen to where it is seen best, but the controls stay in the module that triggered it.
Code for this lesson: App.vue
1<script setup>
2import { ref } from 'vue'
3
4const showModal = ref(false)
5const alertMessage = ref('Oxygen level critical: 15%')
6</script>
7
8<template>
9 <div class="nova-lab">
10 <h1>Teleport Demo</h1>
11
12 <div class="module-panel">
13 <h2>Oxygen Module</h2>
14 <p>Level: 15% - CRITICAL</p>
15 <button @click="showModal = true" class="btn alert">
16 Show Critical Alert
17 </button>
18 </div>
19
20 <div class="module-panel">
21 <h2>Water Module</h2>
22 <p>Level: 82% - Normal</p>
23 </div>
24
25 <!-- Modal teleported to body -->
26 <Teleport to="body">
27 <div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
28 <div class="modal">
29 <h3>CRITICAL ALERT</h3>
30 <p>{{ alertMessage }}</p>
31 <p>Immediate action required!</p>
32 <button @click="showModal = false" class="btn">
33 Dismiss Alert
34 </button>
35 </div>
36 </div>
37 </Teleport>
38 </div>
39</template>
40
41<style scoped>
42.nova-lab {
43 background: #0a0e27;
44 color: #00ff88;
45 min-height: 100vh;
46 padding: 20px;
47 font-family: monospace;
48}
49h1 { color: #00ff88; margin-bottom: 20px; }
50.module-panel {
51 border: 2px solid #00b4d8;
52 padding: 15px;
53 margin: 10px 0;
54 background: rgba(0, 180, 216, 0.05);
55}
56.module-panel h2 { color: #00b4d8; margin-bottom: 8px; }
57.btn {
58 background: #00b4d8;
59 color: #0a0e27;
60 border: none;
61 padding: 10px 20px;
62 cursor: pointer;
63 font-family: monospace;
64 font-weight: bold;
65}
66.btn.alert {
67 background: #ff4444;
68 color: white;
69}
70</style>
71
72<style>
73/* Global styles for teleported modal */
74.modal-overlay {
75 position: fixed;
76 inset: 0;
77 background: rgba(0, 0, 0, 0.8);
78 display: flex;
79 align-items: center;
80 justify-content: center;
81 z-index: 9999;
82}
83.modal {
84 background: #1a1a2e;
85 border: 3px solid #ff4444;
86 padding: 30px;
87 max-width: 400px;
88 text-align: center;
89 color: #00ff88;
90 font-family: monospace;
91}
92.modal h3 {
93 color: #ff4444;
94 margin-bottom: 15px;
95 font-size: 20px;
96 letter-spacing: 2px;
97}
98.modal p { margin: 8px 0; }
99.modal button {
100 margin-top: 15px;
101 background: #ff4444;
102 color: white;
103 border: none;
104 padding: 10px 25px;
105 cursor: pointer;
106 font-family: monospace;
107 font-weight: bold;
108}
109</style>Spotted a mistake in this lesson?
Check yourself
Answer the questions from this lesson. Pick an answer to see right away whether it is correct.
1. What does the <KeepAlive> component do when wrapping a dynamic component?
2. What is <Teleport> used for in Vue?
These are 2 of 3 questions for this lesson. Solve the rest in the game.
Hands-on tasks in the game
- Code editor
Wrap <component :is> in <KeepAlive> and verify that the form state is preserved after switching tabs.
- Horizontal ordering
Arrange the KeepAlive syntax with a dynamic component:
- Vertical ordering
Arrange the events when switching a component inside <KeepAlive>:
- Click in order
Arrange the steps for implementing a modal with Teleport:
- Code editor
Create an AlertModal component using <Teleport to="body"> to render the modal outside the component hierarchy.