Vue.js course Β· Module 8: Slots & Dynamic Components

Teleport

4 min read
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. 1. What does the <KeepAlive> component do when wrapping a dynamic component?

  2. 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.

Useful articles